Refactor AI Assist to background-task + buffer architecture
The assist flow previously tied the entire LLM generation to a single
POST request with no keepalives, causing NS_ERROR_NET_PARTIAL_TRANSFER
in Firefox when Hypercorn closed the connection during gaps between
chunks. This refactor decouples generation into a background task with
a buffer and a separate SSE stream — the same pattern used by chat.
- generation_buffer.py: Widen _buffers to support string keys, add
create/get/remove_assist_buffer() using "assist:{user_id}" keys,
fix cleanup log format for string keys
- generation_task.py: Add run_assist_generation() — lightweight
background task with no DB persistence or title generation
- notes.py: Replace single POST SSE route with POST /api/notes/assist
(returns 202) + GET /api/notes/assist/stream (SSE with 15s keepalives
and Last-Event-ID reconnection); 409 if already running
- useAssist.ts: Switch from apiStreamPost to apiPost + apiSSEStream
two-step pattern with named event mapping and stream handle cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ref, computed, watch, type Ref } from "vue";
|
||||
import { apiStreamPost } from "@/api/client";
|
||||
import { apiPost, apiSSEStream, type SSEStreamHandle } from "@/api/client";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
import {
|
||||
parseMarkdownSections,
|
||||
@@ -28,6 +28,7 @@ export function useAssist(body: Ref<string>) {
|
||||
|
||||
// Snapshot of body at the time a section/selection was chosen
|
||||
let bodySnapshot = "";
|
||||
let streamHandle: SSEStreamHandle | null = null;
|
||||
|
||||
const target = computed<AssistTarget | null>(() => {
|
||||
if (customSelection.value) {
|
||||
@@ -79,6 +80,10 @@ export function useAssist(body: Ref<string>) {
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if (streamHandle) {
|
||||
streamHandle.close();
|
||||
streamHandle = null;
|
||||
}
|
||||
selectedSection.value = null;
|
||||
customSelection.value = null;
|
||||
instruction.value = "";
|
||||
@@ -97,28 +102,33 @@ export function useAssist(body: Ref<string>) {
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
await apiStreamPost(
|
||||
"/api/notes/assist",
|
||||
{
|
||||
body: body.value,
|
||||
target_section: target.value.text,
|
||||
instruction: instruction.value,
|
||||
},
|
||||
(data) => {
|
||||
if (data.chunk) {
|
||||
streamingText.value += data.chunk as string;
|
||||
}
|
||||
if (data.done) {
|
||||
proposedText.value = (data.full_text as string) || streamingText.value;
|
||||
state.value = "review";
|
||||
}
|
||||
if (data.error) {
|
||||
error.value = data.error as string;
|
||||
state.value = "idle";
|
||||
}
|
||||
// Step 1: Launch background generation
|
||||
await apiPost("/api/notes/assist", {
|
||||
body: body.value,
|
||||
target_section: target.value.text,
|
||||
instruction: instruction.value,
|
||||
});
|
||||
|
||||
// Step 2: Tail the SSE buffer
|
||||
streamHandle = apiSSEStream("/api/notes/assist/stream", (evt) => {
|
||||
if (evt.event === "chunk") {
|
||||
streamingText.value += evt.data.chunk as string;
|
||||
}
|
||||
);
|
||||
// If stream ended without a done event, use accumulated text
|
||||
if (evt.event === "done") {
|
||||
proposedText.value = (evt.data.full_text as string) || streamingText.value;
|
||||
state.value = "review";
|
||||
streamHandle = null;
|
||||
}
|
||||
if (evt.event === "error") {
|
||||
error.value = evt.data.error as string;
|
||||
state.value = "idle";
|
||||
streamHandle = null;
|
||||
}
|
||||
});
|
||||
|
||||
await streamHandle.done;
|
||||
|
||||
// Fallback: if stream ended without done/error, use accumulated text
|
||||
if (state.value === "streaming") {
|
||||
if (streamingText.value) {
|
||||
proposedText.value = streamingText.value;
|
||||
@@ -126,10 +136,12 @@ export function useAssist(body: Ref<string>) {
|
||||
} else {
|
||||
state.value = "idle";
|
||||
}
|
||||
streamHandle = null;
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Request failed";
|
||||
state.value = "idle";
|
||||
streamHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user