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:
2026-05-27 10:44:56 +02:00
parent f46f57eded
commit 8223e72fe3
3 changed files with 221 additions and 2 deletions
+158
View File
@@ -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
View File
@@ -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) {
+1
View File
@@ -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))