feat(api): add completion request cancellation and mermaid rendering

Add support for cancelling in-progress LLM completion requests via new /v1/completions/cancel endpoint with task tracking. Implement mermaid diagram rendering in the Milkdown editor with a new mermaidPlugin. Update copilotPlugin to properly abort requests with descriptive reasons. Refactor settings panel to handle system theme changes reactively. Add camera capture support for image uploads.
This commit is contained in:
2026-02-25 19:00:17 +08:00
parent e28125079c
commit 637456ee34
13 changed files with 2013 additions and 147 deletions

View File

@@ -1,7 +1,51 @@
import { API_URL } from './config.js'
import { useSettingsStore } from '../stores/settings'
const API_KEY = 'your-secret-key-here'
let cachedIP = null
function generateRequestId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}
function getCancelUrl(apiUrl) {
const normalized = String(apiUrl || '').replace(/\/+$/, '')
if (!normalized) return '/v1/completions/cancel'
if (normalized.endsWith('/v1/completions')) {
return `${normalized}/cancel`
}
return `${normalized}/cancel`
}
function normalizeAbortReason(reason) {
if (typeof reason === 'string' && reason.trim()) {
return reason.trim().slice(0, 64)
}
return 'abort'
}
async function sendCancelRequest(cancelUrl, requestId, reason) {
try {
await fetch(cancelUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({
request_id: requestId,
reason,
}),
})
} catch (e) {
console.debug('[Copilot] cancel request failed', e)
}
}
async function getClientIP() {
if (cachedIP) return cachedIP
try {
@@ -16,15 +60,30 @@ async function getClientIP() {
}
}
import { useSettingsStore } from '../stores/settings'
export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) {
const requestId = generateRequestId()
const cancelUrl = getCancelUrl(apiUrl)
const onAbort = () => {
const reason = normalizeAbortReason(signal?.reason)
void sendCancelRequest(cancelUrl, requestId, reason)
}
if (signal) {
if (signal.aborted) {
onAbort()
} else {
signal.addEventListener('abort', onAbort, { once: true })
}
}
try {
const settings = useSettingsStore()
const clientIP = await getClientIP()
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'your-secret-key-here'
'X-API-Key': API_KEY,
'X-Request-Id': requestId,
}
// Only send IP if privacy mode is OFF
@@ -41,15 +100,15 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL)
user_preferences: {
language: settings.language,
currency: settings.currency,
timezone: settings.detectedTimezone
}
timezone: settings.detectedTimezone,
},
}
const res = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify(body),
signal
signal,
})
if (!res.ok) {
@@ -95,5 +154,9 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL)
} else {
throw e
}
} finally {
if (signal) {
signal.removeEventListener('abort', onAbort)
}
}
}