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:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
|
||||
@@ -7,6 +7,7 @@ const store = useSettingsStore()
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
const isOpen = ref(false)
|
||||
let systemThemeMediaQuery = null
|
||||
|
||||
const togglePanel = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
@@ -16,15 +17,101 @@ const closePanel = () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
// Theme Handling
|
||||
watch(() => store.theme, (newVal) => {
|
||||
if (newVal === 'system') {
|
||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
setTheme(isDark ? 'dark' : 'light')
|
||||
} else {
|
||||
setTheme(newVal)
|
||||
const applyThemeByPreference = () => {
|
||||
if (store.theme === 'system') {
|
||||
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
setTheme(isDark ? 'dark' : 'light')
|
||||
return
|
||||
}
|
||||
setTheme('light')
|
||||
return
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
setTheme(store.theme)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.theme,
|
||||
() => {
|
||||
applyThemeByPreference()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleSystemThemeChange = (event) => {
|
||||
if (store.theme !== 'system') return
|
||||
setTheme(event.matches ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
|
||||
|
||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
if (typeof systemThemeMediaQuery.addEventListener === 'function') {
|
||||
systemThemeMediaQuery.addEventListener('change', handleSystemThemeChange)
|
||||
} else if (typeof systemThemeMediaQuery.addListener === 'function') {
|
||||
systemThemeMediaQuery.addListener(handleSystemThemeChange)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!systemThemeMediaQuery) return
|
||||
|
||||
if (typeof systemThemeMediaQuery.removeEventListener === 'function') {
|
||||
systemThemeMediaQuery.removeEventListener('change', handleSystemThemeChange)
|
||||
} else if (typeof systemThemeMediaQuery.removeListener === 'function') {
|
||||
systemThemeMediaQuery.removeListener(handleSystemThemeChange)
|
||||
}
|
||||
})
|
||||
|
||||
const appearanceMode = computed({
|
||||
get() {
|
||||
if (store.backgroundType === 'warm') return 'warm'
|
||||
if (store.backgroundType === 'reading') return 'reading'
|
||||
if (store.backgroundType === 'image') return 'image'
|
||||
if (store.theme === 'dark') return 'dark'
|
||||
if (store.theme === 'light') return 'light'
|
||||
return 'system'
|
||||
},
|
||||
set(mode) {
|
||||
if (mode === 'dark') {
|
||||
store.theme = 'dark'
|
||||
store.backgroundType = 'default'
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'light') {
|
||||
store.theme = 'light'
|
||||
store.backgroundType = 'default'
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'system') {
|
||||
store.theme = 'system'
|
||||
store.backgroundType = 'default'
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'warm') {
|
||||
store.theme = 'light'
|
||||
store.backgroundType = 'warm'
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'reading') {
|
||||
store.theme = 'light'
|
||||
store.backgroundType = 'reading'
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'image') {
|
||||
store.theme = 'light'
|
||||
store.backgroundType = 'image'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Background Image Handling
|
||||
const handleImageUpload = (event) => {
|
||||
@@ -41,12 +128,6 @@ const handleImageUpload = (event) => {
|
||||
|
||||
// Helper to translate
|
||||
const t = (key) => store.t[key]
|
||||
|
||||
// Background Style for App (This will be used in App.vue, but we preview it here or just logical check)
|
||||
// UI Helpers
|
||||
const tabs = ['General', 'Model', 'Appearance', 'About']
|
||||
const currentTab = ref('General')
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -88,25 +169,18 @@ const currentTab = ref('General')
|
||||
<h3>{{ t('appearance') }}</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ t('theme') }}</label>
|
||||
<div class="segment-control">
|
||||
<button :class="{ active: store.theme === 'light' }" @click="store.theme = 'light'">{{ t('light') }}</button>
|
||||
<button :class="{ active: store.theme === 'dark' }" @click="store.theme = 'dark'">{{ t('dark') }}</button>
|
||||
<button :class="{ active: store.theme === 'system' }" @click="store.theme = 'system'">{{ t('system') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ t('background') }}</label>
|
||||
<select v-model="store.backgroundType" class="select-input">
|
||||
<option value="default">{{ t('default') }}</option>
|
||||
<label>{{ t('appearance') }}</label>
|
||||
<select v-model="appearanceMode" class="select-input">
|
||||
<option value="dark">{{ t('dark') }}</option>
|
||||
<option value="light">{{ t('light') }}</option>
|
||||
<option value="system">{{ t('system') }}</option>
|
||||
<option value="warm">{{ t('warm') }}</option>
|
||||
<option value="reading">{{ t('reading') }}</option>
|
||||
<option value="image">{{ t('image') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="store.backgroundType === 'image'" class="form-group">
|
||||
<div v-if="appearanceMode === 'image'" class="form-group">
|
||||
<label>{{ t('image') }}</label>
|
||||
<input type="file" accept="image/*" @change="handleImageUpload" class="file-input" />
|
||||
|
||||
@@ -488,3 +562,4 @@ const currentTab = ref('General')
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user