AI chat panel with Edit/Chat modes + verify dropdown
- AI chat panel at bottom of editor (all 3 modes) - Edit mode: AI modifies document directly (no explanations) - Chat mode: AI answers questions about the document - Verify dropdown: Spec Review, Grammar & Spelling, Summary - Enter sends, Shift+Enter for newline - /api/ai/chat endpoint with edit/chat system prompts - Grammar and spec verify actions added to /api/ai/generate
This commit is contained in:
@@ -119,6 +119,30 @@
|
|||||||
<p v-else style="color:#6c7086;font-style:italic">Preview will appear here...</p>
|
<p v-else style="color:#6c7086;font-style:italic">Preview will appear here...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- AI Chat Panel -->
|
||||||
|
<div class="ai-chat-panel" v-if="currentFile">
|
||||||
|
<div class="ai-chat-modes">
|
||||||
|
<button :class="{active: aiChatMode === 'edit'}" @click="aiChatMode = 'edit'">Edit</button>
|
||||||
|
<button :class="{active: aiChatMode === 'chat'}" @click="aiChatMode = 'chat'">Chat</button>
|
||||||
|
<select v-model="aiVerifyType" @change="runVerify" class="ai-verify-select">
|
||||||
|
<option value="" disabled>Verify ▾</option>
|
||||||
|
<option value="spec">Spec Review</option>
|
||||||
|
<option value="grammar">Grammar & Spelling</option>
|
||||||
|
<option value="summary">Summary</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="aiChatLoading" class="ai-loading">⏳</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-chat-output" v-if="aiChatResponse" v-html="renderMarkdown(aiChatResponse)"></div>
|
||||||
|
<div class="ai-chat-input">
|
||||||
|
<textarea
|
||||||
|
v-model="aiChatInput"
|
||||||
|
@keydown="aiChatKeydown"
|
||||||
|
placeholder="Ask AI to edit or chat... (Enter to send, Shift+Enter for newline)"
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
<button @click="sendAiChat" class="ai-send-btn">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- History Panel -->
|
<!-- History Panel -->
|
||||||
<div class="history-panel" v-if="showHistory">
|
<div class="history-panel" v-if="showHistory">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -943,6 +967,12 @@ async function shareFile() {
|
|||||||
|
|
||||||
// ─── AI ──────────────────────────────────────────────────────────────────────
|
// ─── AI ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const aiChatMode = ref('edit')
|
||||||
|
const aiChatInput = ref('')
|
||||||
|
const aiChatResponse = ref('')
|
||||||
|
const aiChatLoading = ref(false)
|
||||||
|
const aiVerifyType = ref('')
|
||||||
|
|
||||||
async function aiVerify() {
|
async function aiVerify() {
|
||||||
if (!currentFile.value) return
|
if (!currentFile.value) return
|
||||||
aiResult.value = 'Verifying...'
|
aiResult.value = 'Verifying...'
|
||||||
@@ -954,6 +984,55 @@ async function aiVerify() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runVerify() {
|
||||||
|
if (!aiVerifyType.value || !currentFile.value) return
|
||||||
|
aiChatLoading.value = true
|
||||||
|
aiChatResponse.value = ''
|
||||||
|
try {
|
||||||
|
const res = await api('/api/ai/generate', { path: currentFile.value, action: aiVerifyType.value })
|
||||||
|
aiChatResponse.value = res.result || res.feedback || 'No response'
|
||||||
|
} catch (e) {
|
||||||
|
aiChatResponse.value = 'AI request failed.'
|
||||||
|
}
|
||||||
|
aiChatLoading.value = false
|
||||||
|
aiVerifyType.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function aiChatKeydown(e) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
sendAiChat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAiChat() {
|
||||||
|
const msg = aiChatInput.value.trim()
|
||||||
|
if (!msg) return
|
||||||
|
aiChatLoading.value = true
|
||||||
|
aiChatResponse.value = ''
|
||||||
|
aiChatInput.value = ''
|
||||||
|
|
||||||
|
const action = aiChatMode.value === 'edit' ? 'edit' : 'chat'
|
||||||
|
try {
|
||||||
|
const res = await api('/api/ai/chat', {
|
||||||
|
path: currentFile.value,
|
||||||
|
content: content.value,
|
||||||
|
message: msg,
|
||||||
|
mode: action,
|
||||||
|
})
|
||||||
|
if (action === 'edit' && res.content) {
|
||||||
|
content.value = res.content
|
||||||
|
isDirty.value = true
|
||||||
|
aiChatResponse.value = 'Document updated.'
|
||||||
|
} else {
|
||||||
|
aiChatResponse.value = res.result || res.content || 'No response'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
aiChatResponse.value = 'AI request failed. Check MH_AI_ENDPOINT.'
|
||||||
|
}
|
||||||
|
aiChatLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Collab ──────────────────────────────────────────────────────────────────
|
// ─── Collab ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function toggleCollab() {
|
function toggleCollab() {
|
||||||
@@ -1465,6 +1544,85 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
}
|
}
|
||||||
.git-btn.dirty { border-color: var(--danger); }
|
.git-btn.dirty { border-color: var(--danger); }
|
||||||
|
|
||||||
|
/* ─── AI Chat Panel ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.ai-chat-panel {
|
||||||
|
height: 15%;
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
.ai-chat-modes {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ai-chat-modes button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.ai-chat-modes button.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.ai-verify-select {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ai-loading { font-size: 12px; }
|
||||||
|
.ai-chat-output {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.ai-chat-output p { margin: 0 0 6px; }
|
||||||
|
.ai-chat-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ai-chat-input textarea {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.ai-send-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.history-panel, .share-dialog, .ai-panel {
|
.history-panel, .share-dialog, .ai-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
+62
-2
@@ -98,12 +98,21 @@ func (s *Server) handleAIGenerate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
systemPrompt := "You are a helpful assistant. Respond in markdown."
|
systemPrompt := "You are a helpful assistant. Respond in markdown."
|
||||||
switch req.Action {
|
switch req.Action {
|
||||||
case "summarize":
|
case "summarize", "summary":
|
||||||
systemPrompt = "Summarize the following text concisely in markdown."
|
systemPrompt = "Summarize the following text concisely in markdown."
|
||||||
case "prompt":
|
case "prompt":
|
||||||
systemPrompt = "Generate a detailed AI prompt based on the following specification. The prompt should instruct an AI coding agent to implement the project."
|
systemPrompt = "Generate a detailed AI prompt based on the following specification. The prompt should instruct an AI coding agent to implement the project."
|
||||||
case "expand":
|
case "expand":
|
||||||
systemPrompt = "Expand on the following text with more detail, examples, and explanations. Respond in markdown."
|
systemPrompt = "Expand on the following text with more detail, examples, and explanations. Respond in markdown."
|
||||||
|
case "grammar":
|
||||||
|
systemPrompt = "Review the following text for grammar and spelling errors. List each error with the correction. Be concise. Format as a markdown list."
|
||||||
|
case "spec":
|
||||||
|
systemPrompt = `You are a technical reviewer. Review the following specification document for:
|
||||||
|
1. Completeness - are there missing details needed to implement this?
|
||||||
|
2. Ambiguities - are there unclear requirements?
|
||||||
|
3. Feasibility - is this technically achievable?
|
||||||
|
4. Suggestions - any improvements?
|
||||||
|
Respond with a structured review. End with a clear verdict: READY TO BUILD or NEEDS REVISION.`
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := callLLM(aiEndpoint, aiKey, aiModel, systemPrompt, inputText)
|
response, err := callLLM(aiEndpoint, aiKey, aiModel, systemPrompt, inputText)
|
||||||
@@ -118,7 +127,58 @@ func (s *Server) handleAIGenerate(w http.ResponseWriter, r *http.Request) {
|
|||||||
files.WriteFile(s.dataDir, userID, filename, response)
|
files.WriteFile(s.dataDir, userID, filename, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, 200, map[string]string{"output": response})
|
writeJSON(w, 200, map[string]string{"result": response})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Mode string `json:"mode"` // "edit" or "chat"
|
||||||
|
}
|
||||||
|
if err := decodeBody(r, &req); err != nil || req.Message == "" {
|
||||||
|
writeJSON(w, 400, map[string]string{"error": "message required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
aiEndpoint := os.Getenv("MH_AI_ENDPOINT")
|
||||||
|
aiKey := os.Getenv("MH_AI_API_KEY")
|
||||||
|
aiModel := os.Getenv("MH_AI_MODEL")
|
||||||
|
if aiEndpoint == "" {
|
||||||
|
writeJSON(w, 500, map[string]string{"error": "AI endpoint not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if aiModel == "" {
|
||||||
|
aiModel = "gpt-4"
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemPrompt string
|
||||||
|
var userMsg string
|
||||||
|
|
||||||
|
if req.Mode == "edit" {
|
||||||
|
systemPrompt = `You are a document editor. The user will give you a markdown document and an instruction.
|
||||||
|
Apply the instruction to the document and return ONLY the complete updated document.
|
||||||
|
Do not add explanations, comments, or markdown code fences around the output.
|
||||||
|
Preserve the document's existing style and formatting.`
|
||||||
|
userMsg = "Document:\n\n" + req.Content + "\n\nInstruction: " + req.Message
|
||||||
|
} else {
|
||||||
|
systemPrompt = `You are a helpful writing assistant. The user has a markdown document open and is asking a question about it.
|
||||||
|
Answer concisely in markdown. Reference the document content when relevant.`
|
||||||
|
userMsg = "Document:\n\n" + req.Content + "\n\nQuestion: " + req.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := callLLM(aiEndpoint, aiKey, aiModel, systemPrompt, userMsg)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, 500, map[string]string{"error": "AI call failed: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Mode == "edit" {
|
||||||
|
writeJSON(w, 200, map[string]string{"content": response})
|
||||||
|
} else {
|
||||||
|
writeJSON(w, 200, map[string]string{"result": response})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func callLLM(endpoint, apiKey, model, systemPrompt, userContent string) (string, error) {
|
func callLLM(endpoint, apiKey, model, systemPrompt, userContent string) (string, error) {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func NewRouter(db *sql.DB, dataDir, secret string) http.Handler {
|
|||||||
// AI
|
// AI
|
||||||
mux.HandleFunc("POST /api/ai/verify", s.requireAuth(s.handleAIVerify))
|
mux.HandleFunc("POST /api/ai/verify", s.requireAuth(s.handleAIVerify))
|
||||||
mux.HandleFunc("POST /api/ai/generate", s.requireAuth(s.handleAIGenerate))
|
mux.HandleFunc("POST /api/ai/generate", s.requireAuth(s.handleAIGenerate))
|
||||||
|
mux.HandleFunc("POST /api/ai/chat", s.requireAuth(s.handleAIChat))
|
||||||
|
|
||||||
// Build jobs
|
// Build jobs
|
||||||
mux.HandleFunc("POST /api/build/submit", s.requireAuth(s.handleBuildSubmit))
|
mux.HandleFunc("POST /api/build/submit", s.requireAuth(s.handleBuildSubmit))
|
||||||
|
|||||||
Reference in New Issue
Block a user