mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
Compare commits
14 Commits
fix/chat-l
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d43bcfb87 | ||
|
|
5406778dd6 | ||
|
|
bb65a8c07a | ||
|
|
4e223b6237 | ||
|
|
c60e3930a3 | ||
|
|
5c8ae4d6d7 | ||
|
|
f965f3fa2e | ||
|
|
73eefc7aa6 | ||
|
|
a8d27088ef | ||
|
|
d4454beb9a | ||
|
|
171174378c | ||
|
|
eadc2c2629 | ||
|
|
c77af86011 | ||
|
|
0cd1260172 |
@@ -10,8 +10,8 @@ import {
|
|||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable"
|
} from "@/components/ui/resizable"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { type DrawioTheme, isDrawioTheme } from "@/lib/drawio-themes"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
import { isIndexedDBUsable } from "@/lib/session-storage"
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const {
|
const {
|
||||||
@@ -27,13 +27,11 @@ export default function Home() {
|
|||||||
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
|
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<DrawioTheme>("kennedy")
|
||||||
const [darkMode, setDarkMode] = useState(false)
|
const [darkMode, setDarkMode] = useState(false)
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||||
const [isElectron, setIsElectron] = useState(false)
|
const [isElectron, setIsElectron] = useState(false)
|
||||||
const [canPersist, setCanPersist] = useState(false)
|
|
||||||
const [canPersistChecked, setCanPersistChecked] = useState(false)
|
|
||||||
const [drawioBaseUrl, setDrawioBaseUrl] = useState(
|
const [drawioBaseUrl, setDrawioBaseUrl] = useState(
|
||||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net",
|
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net",
|
||||||
)
|
)
|
||||||
@@ -56,7 +54,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const savedUi = localStorage.getItem("drawio-theme")
|
const savedUi = localStorage.getItem("drawio-theme")
|
||||||
if (savedUi === "min" || savedUi === "sketch") {
|
if (isDrawioTheme(savedUi)) {
|
||||||
setDrawioUi(savedUi)
|
setDrawioUi(savedUi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +82,6 @@ export default function Home() {
|
|||||||
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
|
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
const usable = await isIndexedDBUsable()
|
|
||||||
setCanPersist(usable)
|
|
||||||
setCanPersistChecked(true)
|
|
||||||
})()
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
}, [pathname, router])
|
}, [pathname, router])
|
||||||
|
|
||||||
@@ -97,13 +90,6 @@ export default function Home() {
|
|||||||
onDrawioLoad()
|
onDrawioLoad()
|
||||||
}, [onDrawioLoad])
|
}, [onDrawioLoad])
|
||||||
|
|
||||||
const handleDrawioAutoSave = useCallback(
|
|
||||||
(data: { xml?: string }) => {
|
|
||||||
handleDiagramAutoSave(data)
|
|
||||||
},
|
|
||||||
[handleDiagramAutoSave],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDarkModeChange = () => {
|
const handleDarkModeChange = () => {
|
||||||
const newValue = !darkMode
|
const newValue = !darkMode
|
||||||
setDarkMode(newValue)
|
setDarkMode(newValue)
|
||||||
@@ -113,10 +99,9 @@ export default function Home() {
|
|||||||
resetDrawioReady()
|
resetDrawioReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrawioUiChange = () => {
|
const handleDrawioUiChange = (theme: DrawioTheme) => {
|
||||||
const newUi = drawioUi === "min" ? "sketch" : "min"
|
localStorage.setItem("drawio-theme", theme)
|
||||||
localStorage.setItem("drawio-theme", newUi)
|
setDrawioUi(theme)
|
||||||
setDrawioUi(newUi)
|
|
||||||
setIsDrawioReady(false)
|
setIsDrawioReady(false)
|
||||||
resetDrawioReady()
|
resetDrawioReady()
|
||||||
}
|
}
|
||||||
@@ -187,7 +172,7 @@ export default function Home() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
|
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
|
||||||
{isLoaded && canPersistChecked && (
|
{isLoaded && (
|
||||||
<div
|
<div
|
||||||
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
|
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
|
||||||
>
|
>
|
||||||
@@ -195,28 +180,19 @@ export default function Home() {
|
|||||||
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
|
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
autosave
|
autosave
|
||||||
onAutoSave={handleDrawioAutoSave}
|
onAutoSave={handleDiagramAutoSave}
|
||||||
onExport={handleDiagramExport}
|
onExport={handleDiagramExport}
|
||||||
onLoad={handleDrawioLoad}
|
onLoad={handleDrawioLoad}
|
||||||
baseUrl={drawioBaseUrl}
|
baseUrl={drawioBaseUrl}
|
||||||
configuration={
|
|
||||||
canPersist
|
|
||||||
? { confirmExit: false }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
urlParameters={{
|
urlParameters={{
|
||||||
ui: drawioUi,
|
ui: drawioUi,
|
||||||
spin: false,
|
spin: false,
|
||||||
libraries: false,
|
libraries: false,
|
||||||
// Disable modified tracking only when persistence is available
|
|
||||||
...(canPersist && {
|
|
||||||
modified: false,
|
|
||||||
keepmodified: false,
|
|
||||||
}),
|
|
||||||
saveAndExit: false,
|
saveAndExit: false,
|
||||||
noSaveBtn: true,
|
noSaveBtn: true,
|
||||||
noExitBtn: true,
|
noExitBtn: true,
|
||||||
dark: darkMode,
|
dark:
|
||||||
|
darkMode || drawioUi === "dark",
|
||||||
lang: currentLang,
|
lang: currentLang,
|
||||||
// Enable offline mode in Electron to disable external service calls
|
// Enable offline mode in Electron to disable external service calls
|
||||||
...(isElectron && {
|
...(isElectron && {
|
||||||
@@ -264,7 +240,7 @@ export default function Home() {
|
|||||||
isVisible={isChatVisible}
|
isVisible={isChatVisible}
|
||||||
onToggleVisibility={toggleChatPanel}
|
onToggleVisibility={toggleChatPanel}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={handleDrawioUiChange}
|
onDrawioUiChange={handleDrawioUiChange}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={handleDarkModeChange}
|
onToggleDarkMode={handleDarkModeChange}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useSessionManager } from "@/hooks/use-session-manager"
|
|||||||
import { useValidateDiagram } from "@/hooks/use-validate-diagram"
|
import { useValidateDiagram } from "@/hooks/use-validate-diagram"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
|
import type { DrawioTheme } from "@/lib/drawio-themes"
|
||||||
import { formatMessage } from "@/lib/i18n/utils"
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
import { sanitizeMessages } from "@/lib/session-storage"
|
import { sanitizeMessages } from "@/lib/session-storage"
|
||||||
@@ -68,8 +69,8 @@ interface ChatMessage {
|
|||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
onToggleVisibility: () => void
|
onToggleVisibility: () => void
|
||||||
drawioUi: "min" | "sketch"
|
drawioUi: DrawioTheme
|
||||||
onToggleDrawioUi: () => void
|
onDrawioUiChange: (theme: DrawioTheme) => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
onToggleDarkMode: () => void
|
onToggleDarkMode: () => void
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
@@ -110,7 +111,7 @@ export default function ChatPanel({
|
|||||||
isVisible,
|
isVisible,
|
||||||
onToggleVisibility,
|
onToggleVisibility,
|
||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onDrawioUiChange,
|
||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
@@ -1442,7 +1443,7 @@ export default function ChatPanel({
|
|||||||
open={showSettingsDialog}
|
open={showSettingsDialog}
|
||||||
onOpenChange={setShowSettingsDialog}
|
onOpenChange={setShowSettingsDialog}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={onToggleDrawioUi}
|
onDrawioUiChange={onDrawioUiChange}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={onToggleDarkMode}
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
minimalStyle={minimalStyle}
|
minimalStyle={minimalStyle}
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ export function ChatLobby({
|
|||||||
currentInput = "",
|
currentInput = "",
|
||||||
dict,
|
dict,
|
||||||
}: ChatLobbyProps) {
|
}: ChatLobbyProps) {
|
||||||
const [templatesExpanded, setTemplatesExpanded] = useState(false)
|
const [templatesExpanded, setTemplatesExpanded] = useState(true)
|
||||||
const [examplesExpanded, setExamplesExpanded] = useState(false)
|
const [examplesExpanded, setExamplesExpanded] = useState(true)
|
||||||
const [panelVisibility, setPanelVisibility] = useState(getPanelVisibility)
|
const [panelVisibility, setPanelVisibility] = useState(getPanelVisibility)
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [sessionToDelete, setSessionToDelete] = useState<string | null>(null)
|
const [sessionToDelete, setSessionToDelete] = useState<string | null>(null)
|
||||||
@@ -125,19 +125,25 @@ export function ChatLobby({
|
|||||||
const hasHistory = sessions.length > 0
|
const hasHistory = sessions.length > 0
|
||||||
|
|
||||||
if (!hasHistory) {
|
if (!hasHistory) {
|
||||||
if (panelVisibility.myTemplates) {
|
if (!panelVisibility.myTemplates && !panelVisibility.quickExamples) {
|
||||||
return (
|
return null
|
||||||
<TemplatePanel
|
|
||||||
setInput={setInput}
|
|
||||||
onSendTemplate={onSendTemplate}
|
|
||||||
currentInput={currentInput}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (panelVisibility.quickExamples) {
|
return (
|
||||||
return <ExamplePanel setInput={setInput} setFiles={setFiles} />
|
<div className="animate-fade-in">
|
||||||
}
|
{panelVisibility.myTemplates && (
|
||||||
return null
|
<TemplatePanel
|
||||||
|
setInput={setInput}
|
||||||
|
onSendTemplate={onSendTemplate}
|
||||||
|
currentInput={currentInput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{panelVisibility.quickExamples && (
|
||||||
|
<div className={panelVisibility.myTemplates ? "mt-6" : ""}>
|
||||||
|
<ExamplePanel setInput={setInput} setFiles={setFiles} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show history + collapsible examples when there are sessions
|
// Show history + collapsible examples when there are sessions
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ export function ModelConfigDialog({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 px-2">
|
<ScrollArea className="flex-1 px-2 min-h-0">
|
||||||
<div className="space-y-1 pb-2">
|
<div className="space-y-1 pb-2">
|
||||||
{config.providers.length === 0 ? (
|
{config.providers.length === 0 ? (
|
||||||
<div className="px-3 py-8 text-center">
|
<div className="px-3 py-8 text-center">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { Switch } from "@/components/ui/switch"
|
|||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
|
import type { DrawioTheme } from "@/lib/drawio-themes"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
import { STORAGE_KEYS } from "@/lib/storage"
|
import { STORAGE_KEYS } from "@/lib/storage"
|
||||||
|
|
||||||
@@ -63,8 +64,8 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
|
|||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
drawioUi: "min" | "sketch"
|
drawioUi: DrawioTheme
|
||||||
onToggleDrawioUi: () => void
|
onDrawioUiChange: (theme: DrawioTheme) => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
onToggleDarkMode: () => void
|
onToggleDarkMode: () => void
|
||||||
minimalStyle?: boolean
|
minimalStyle?: boolean
|
||||||
@@ -90,7 +91,7 @@ function SettingsContent({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onDrawioUiChange,
|
||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
minimalStyle = false,
|
minimalStyle = false,
|
||||||
@@ -134,8 +135,11 @@ function SettingsContent({
|
|||||||
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch if not cached in localStorage
|
// Re-fetch config whenever the dialog opens to ensure we always show
|
||||||
if (getStoredAccessCodeRequired() !== null) return
|
// the access code input if the server requires it. This fixes the case
|
||||||
|
// where a stale localStorage cache (from before ACCESS_CODE_LIST was
|
||||||
|
// configured) would hide the access code input.
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
fetch(getApiEndpoint("/api/config"))
|
fetch(getApiEndpoint("/api/config"))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -151,10 +155,9 @@ function SettingsContent({
|
|||||||
setAccessCodeRequired(required)
|
setAccessCodeRequired(required)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Don't cache on error - allow retry on next mount
|
// Keep existing cached value on error
|
||||||
setAccessCodeRequired(false)
|
|
||||||
})
|
})
|
||||||
}, [])
|
}, [open])
|
||||||
|
|
||||||
// Detect current language from pathname
|
// Detect current language from pathname
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -430,23 +433,40 @@ function SettingsContent({
|
|||||||
{/* Draw.io Style */}
|
{/* Draw.io Style */}
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={dict.settings.drawioStyle}
|
label={dict.settings.drawioStyle}
|
||||||
description={`${dict.settings.drawioStyleDescription} ${
|
description={dict.settings.drawioStyleDescription}
|
||||||
drawioUi === "min"
|
|
||||||
? dict.settings.minimal
|
|
||||||
: dict.settings.sketch
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Select
|
||||||
id="drawio-ui"
|
value={drawioUi}
|
||||||
variant="outline"
|
onValueChange={(v) =>
|
||||||
onClick={onToggleDrawioUi}
|
onDrawioUiChange(v as DrawioTheme)
|
||||||
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
}
|
||||||
>
|
>
|
||||||
{dict.settings.switchTo}{" "}
|
<SelectTrigger
|
||||||
{drawioUi === "min"
|
id="drawio-ui-select"
|
||||||
? dict.settings.sketch
|
aria-label={dict.settings.drawioStyle}
|
||||||
: dict.settings.minimal}
|
className="w-[120px] h-9 rounded-xl"
|
||||||
</Button>
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="kennedy">
|
||||||
|
{dict.settings.themeDefault}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="atlas">Atlas</SelectItem>
|
||||||
|
<SelectItem value="dark">
|
||||||
|
{dict.settings.themeDark}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="min">
|
||||||
|
{dict.settings.themeMinimal}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="sketch">
|
||||||
|
{dict.settings.themeSketch}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="simple">
|
||||||
|
{dict.settings.themeSimple}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
{/* Diagram Style */}
|
{/* Diagram Style */}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ linux:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
|
- target: rpm
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
|
||||||
# Publish configuration (optional)
|
# Publish configuration (optional)
|
||||||
publish:
|
publish:
|
||||||
|
|||||||
2
electron/electron.d.ts
vendored
2
electron/electron.d.ts
vendored
@@ -101,8 +101,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ConfigPreset,
|
|
||||||
ApplyPresetResult,
|
ApplyPresetResult,
|
||||||
|
ConfigPreset,
|
||||||
ProxyConfig,
|
ProxyConfig,
|
||||||
SetProxyResult,
|
SetProxyResult,
|
||||||
SetUserLocaleResult,
|
SetUserLocaleResult,
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ export function createWindow(serverUrl: string): BrowserWindow {
|
|||||||
mainWindow.webContents.openDevTools()
|
mainWindow.webContents.openDevTools()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override the draw.io iframe's beforeunload handler so the window can
|
||||||
|
// close after the user edits text in a shape (fixes #815). Diagrams are
|
||||||
|
// already persisted via autosave, so the prompt is unnecessary.
|
||||||
|
mainWindow.webContents.on("will-prevent-unload", (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1262,7 +1262,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
case "glm":
|
case "glm":
|
||||||
case "qwen":
|
case "qwen":
|
||||||
case "qiniu":
|
case "qiniu":
|
||||||
case "kimi":
|
|
||||||
case "novita": {
|
case "novita": {
|
||||||
const envVar = PROVIDER_ENV_VARS[provider]
|
const envVar = PROVIDER_ENV_VARS[provider]
|
||||||
if (!envVar) {
|
if (!envVar) {
|
||||||
@@ -1288,6 +1287,23 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "kimi": {
|
||||||
|
const apiKey = resolveApiKey(overrides, "KIMI_API_KEY")
|
||||||
|
const baseURL = resolveBaseURL(
|
||||||
|
overrides?.apiKey,
|
||||||
|
overrides?.baseUrl,
|
||||||
|
resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"),
|
||||||
|
PROVIDER_INFO["kimi"]?.defaultBaseUrl,
|
||||||
|
)
|
||||||
|
// Use createDeepSeek to properly handle reasoning_content for Kimi
|
||||||
|
// thinking models (e.g., kimi-k2.6). Kimi's API uses the same
|
||||||
|
// reasoning_content field as DeepSeek, so this provider correctly
|
||||||
|
// captures and replays reasoning in multi-turn conversations.
|
||||||
|
const customProvider = createDeepSeek({ apiKey, baseURL })
|
||||||
|
model = customProvider(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`,
|
||||||
@@ -1357,10 +1373,12 @@ export function supportsImageInput(modelId: string): boolean {
|
|||||||
|
|
||||||
// Qwen text models (not vision variants like qwen-vl)
|
// Qwen text models (not vision variants like qwen-vl)
|
||||||
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input
|
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input
|
||||||
|
// QvQ (Qwen Visual QA) models are vision models — exclude them even when prefixed with "qwen/"
|
||||||
if (
|
if (
|
||||||
lowerModelId.includes("qwen") &&
|
lowerModelId.includes("qwen") &&
|
||||||
!hasVisionIndicator &&
|
!hasVisionIndicator &&
|
||||||
!lowerModelId.includes("qwen3.5")
|
!lowerModelId.includes("qwen3.5") &&
|
||||||
|
!lowerModelId.includes("qvq")
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
17
lib/drawio-themes.ts
Normal file
17
lib/drawio-themes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const DRAWIO_THEMES = [
|
||||||
|
"kennedy",
|
||||||
|
"atlas",
|
||||||
|
"dark",
|
||||||
|
"min",
|
||||||
|
"sketch",
|
||||||
|
"simple",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type DrawioTheme = (typeof DRAWIO_THEMES)[number]
|
||||||
|
|
||||||
|
export function isDrawioTheme(value: unknown): value is DrawioTheme {
|
||||||
|
return (
|
||||||
|
typeof value === "string" &&
|
||||||
|
(DRAWIO_THEMES as readonly string[]).includes(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
|
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
|
||||||
"drawioStyle": "DrawIO Style",
|
"drawioStyle": "DrawIO Style",
|
||||||
"drawioStyleDescription": "Canvas style:",
|
"drawioStyleDescription": "Canvas style",
|
||||||
"switchTo": "Switch to",
|
"themeDefault": "Default",
|
||||||
"minimal": "Minimal",
|
"themeDark": "Dark",
|
||||||
"sketch": "Sketch",
|
"themeMinimal": "Minimal",
|
||||||
|
"themeSketch": "Sketch",
|
||||||
|
"themeSimple": "Simple",
|
||||||
"diagramStyle": "Diagram Style",
|
"diagramStyle": "Diagram Style",
|
||||||
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
|
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
|
||||||
"sendShortcut": "Send Shortcut",
|
"sendShortcut": "Send Shortcut",
|
||||||
|
|||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
||||||
"drawioStyle": "DrawIO スタイル",
|
"drawioStyle": "DrawIO スタイル",
|
||||||
"drawioStyleDescription": "キャンバススタイル:",
|
"drawioStyleDescription": "キャンバススタイル",
|
||||||
"switchTo": "切り替え",
|
"themeDefault": "デフォルト",
|
||||||
"minimal": "ミニマル",
|
"themeDark": "ダーク",
|
||||||
"sketch": "スケッチ",
|
"themeMinimal": "ミニマル",
|
||||||
|
"themeSketch": "スケッチ",
|
||||||
|
"themeSimple": "シンプル",
|
||||||
"diagramStyle": "ダイアグラムスタイル",
|
"diagramStyle": "ダイアグラムスタイル",
|
||||||
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
||||||
"sendShortcut": "送信ショートカット",
|
"sendShortcut": "送信ショートカット",
|
||||||
|
|||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "主題",
|
"theme": "主題",
|
||||||
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
|
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
|
||||||
"drawioStyle": "DrawIO 樣式",
|
"drawioStyle": "DrawIO 樣式",
|
||||||
"drawioStyleDescription": "畫布樣式:",
|
"drawioStyleDescription": "畫布樣式",
|
||||||
"switchTo": "切換到",
|
"themeDefault": "預設",
|
||||||
"minimal": "簡約",
|
"themeDark": "深色",
|
||||||
"sketch": "草圖",
|
"themeMinimal": "簡約",
|
||||||
|
"themeSketch": "草圖",
|
||||||
|
"themeSimple": "簡單",
|
||||||
"diagramStyle": "圖表樣式",
|
"diagramStyle": "圖表樣式",
|
||||||
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
|
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
|
||||||
"sendShortcut": "傳送快捷鍵",
|
"sendShortcut": "傳送快捷鍵",
|
||||||
|
|||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
||||||
"drawioStyle": "DrawIO 样式",
|
"drawioStyle": "DrawIO 样式",
|
||||||
"drawioStyleDescription": "画布样式:",
|
"drawioStyleDescription": "画布样式",
|
||||||
"switchTo": "切换到",
|
"themeDefault": "默认",
|
||||||
"minimal": "简约",
|
"themeDark": "深色",
|
||||||
"sketch": "草图",
|
"themeMinimal": "简约",
|
||||||
|
"themeSketch": "草图",
|
||||||
|
"themeSimple": "简单",
|
||||||
"diagramStyle": "图表样式",
|
"diagramStyle": "图表样式",
|
||||||
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
||||||
"sendShortcut": "发送快捷键",
|
"sendShortcut": "发送快捷键",
|
||||||
|
|||||||
@@ -58,33 +58,6 @@ interface ChatSessionDB extends DBSchema {
|
|||||||
|
|
||||||
// Database singleton
|
// Database singleton
|
||||||
let dbPromise: Promise<IDBPDatabase<ChatSessionDB>> | null = null
|
let dbPromise: Promise<IDBPDatabase<ChatSessionDB>> | null = null
|
||||||
const resetDBPromise = () => {
|
|
||||||
dbPromise = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const isClosingError = (error: unknown): boolean => {
|
|
||||||
return (
|
|
||||||
error instanceof DOMException &&
|
|
||||||
error.name === "InvalidStateError" &&
|
|
||||||
/closing/i.test(error.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const withDB = async <T>(
|
|
||||||
action: (db: IDBPDatabase<ChatSessionDB>) => Promise<T>,
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const db = await getDB()
|
|
||||||
return await action(db)
|
|
||||||
} catch (error) {
|
|
||||||
if (isClosingError(error)) {
|
|
||||||
resetDBPromise()
|
|
||||||
const db = await getDB()
|
|
||||||
return await action(db)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
|
async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
|
||||||
if (!dbPromise) {
|
if (!dbPromise) {
|
||||||
@@ -115,23 +88,7 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
terminated() {
|
|
||||||
resetDBPromise()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
dbPromise
|
|
||||||
.then((db) => {
|
|
||||||
db.onversionchange = () => {
|
|
||||||
db.close()
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
db.onclose = () => {
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
resetDBPromise()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return dbPromise
|
return dbPromise
|
||||||
}
|
}
|
||||||
@@ -146,46 +103,31 @@ export function isIndexedDBAvailable(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if IndexedDB is actually usable (not just present).
|
|
||||||
// Note: Do NOT close the db here - getDB() returns a shared singleton connection
|
|
||||||
// that other code depends on.
|
|
||||||
export async function isIndexedDBUsable(): Promise<boolean> {
|
|
||||||
if (!isIndexedDBAvailable()) return false
|
|
||||||
try {
|
|
||||||
await getDB()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRUD Operations
|
// CRUD Operations
|
||||||
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
||||||
if (!isIndexedDBAvailable()) return []
|
if (!isIndexedDBAvailable()) return []
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const tx = db.transaction(STORE_NAME, "readonly")
|
const tx = db.transaction(STORE_NAME, "readonly")
|
||||||
const index = tx.store.index("by-updated")
|
const index = tx.store.index("by-updated")
|
||||||
const metadata: SessionMetadata[] = []
|
const metadata: SessionMetadata[] = []
|
||||||
|
|
||||||
// Use cursor to read only metadata fields (avoids loading full messages/XML)
|
// Use cursor to read only metadata fields (avoids loading full messages/XML)
|
||||||
let cursor = await index.openCursor(null, "prev") // newest first
|
let cursor = await index.openCursor(null, "prev") // newest first
|
||||||
while (cursor) {
|
while (cursor) {
|
||||||
const s = cursor.value
|
const s = cursor.value
|
||||||
metadata.push({
|
metadata.push({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title,
|
title: s.title,
|
||||||
createdAt: s.createdAt,
|
createdAt: s.createdAt,
|
||||||
updatedAt: s.updatedAt,
|
updatedAt: s.updatedAt,
|
||||||
messageCount: s.messages.length,
|
messageCount: s.messages.length,
|
||||||
hasDiagram:
|
hasDiagram: !!s.diagramXml && s.diagramXml.trim().length > 0,
|
||||||
!!s.diagramXml && s.diagramXml.trim().length > 0,
|
thumbnailDataUrl: s.thumbnailDataUrl,
|
||||||
thumbnailDataUrl: s.thumbnailDataUrl,
|
})
|
||||||
})
|
cursor = await cursor.continue()
|
||||||
cursor = await cursor.continue()
|
}
|
||||||
}
|
return metadata
|
||||||
return metadata
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get session metadata:", error)
|
console.error("Failed to get session metadata:", error)
|
||||||
return []
|
return []
|
||||||
@@ -195,9 +137,8 @@ export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
|||||||
export async function getSession(id: string): Promise<ChatSession | null> {
|
export async function getSession(id: string): Promise<ChatSession | null> {
|
||||||
if (!isIndexedDBAvailable()) return null
|
if (!isIndexedDBAvailable()) return null
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
return (await db.get(STORE_NAME, id)) || null
|
return (await db.get(STORE_NAME, id)) || null
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get session:", error)
|
console.error("Failed to get session:", error)
|
||||||
return null
|
return null
|
||||||
@@ -207,9 +148,8 @@ export async function getSession(id: string): Promise<ChatSession | null> {
|
|||||||
export async function saveSession(session: ChatSession): Promise<boolean> {
|
export async function saveSession(session: ChatSession): Promise<boolean> {
|
||||||
if (!isIndexedDBAvailable()) return false
|
if (!isIndexedDBAvailable()) return false
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.put(STORE_NAME, session)
|
await db.put(STORE_NAME, session)
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle quota exceeded
|
// Handle quota exceeded
|
||||||
@@ -221,9 +161,8 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
|
|||||||
await deleteOldestSession()
|
await deleteOldestSession()
|
||||||
// Retry once
|
// Retry once
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.put(STORE_NAME, session)
|
await db.put(STORE_NAME, session)
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -242,9 +181,8 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
|
|||||||
export async function deleteSession(id: string): Promise<void> {
|
export async function deleteSession(id: string): Promise<void> {
|
||||||
if (!isIndexedDBAvailable()) return
|
if (!isIndexedDBAvailable()) return
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.delete(STORE_NAME, id)
|
await db.delete(STORE_NAME, id)
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete session:", error)
|
console.error("Failed to delete session:", error)
|
||||||
}
|
}
|
||||||
@@ -253,9 +191,8 @@ export async function deleteSession(id: string): Promise<void> {
|
|||||||
export async function getSessionCount(): Promise<number> {
|
export async function getSessionCount(): Promise<number> {
|
||||||
if (!isIndexedDBAvailable()) return 0
|
if (!isIndexedDBAvailable()) return 0
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
return await db.count(STORE_NAME)
|
return await db.count(STORE_NAME)
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get session count:", error)
|
console.error("Failed to get session count:", error)
|
||||||
return 0
|
return 0
|
||||||
@@ -265,15 +202,14 @@ export async function getSessionCount(): Promise<number> {
|
|||||||
export async function deleteOldestSession(): Promise<void> {
|
export async function deleteOldestSession(): Promise<void> {
|
||||||
if (!isIndexedDBAvailable()) return
|
if (!isIndexedDBAvailable()) return
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const tx = db.transaction(STORE_NAME, "readwrite")
|
const tx = db.transaction(STORE_NAME, "readwrite")
|
||||||
const index = tx.store.index("by-updated")
|
const index = tx.store.index("by-updated")
|
||||||
const cursor = await index.openCursor()
|
const cursor = await index.openCursor()
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
await cursor.delete()
|
await cursor.delete()
|
||||||
}
|
}
|
||||||
await tx.done
|
await tx.done
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete oldest session:", error)
|
console.error("Failed to delete oldest session:", error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,33 +57,6 @@ export function generateDefaultTitle(prompt: string): string {
|
|||||||
|
|
||||||
// Database singleton
|
// Database singleton
|
||||||
let dbPromise: Promise<IDBPDatabase<TemplateDB>> | null = null
|
let dbPromise: Promise<IDBPDatabase<TemplateDB>> | null = null
|
||||||
const resetDBPromise = () => {
|
|
||||||
dbPromise = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const isClosingError = (error: unknown): boolean => {
|
|
||||||
return (
|
|
||||||
error instanceof DOMException &&
|
|
||||||
error.name === "InvalidStateError" &&
|
|
||||||
/closing/i.test(error.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const withDB = async <T>(
|
|
||||||
action: (db: IDBPDatabase<TemplateDB>) => Promise<T>,
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const db = await getDB()
|
|
||||||
return await action(db)
|
|
||||||
} catch (error) {
|
|
||||||
if (isClosingError(error)) {
|
|
||||||
resetDBPromise()
|
|
||||||
const db = await getDB()
|
|
||||||
return await action(db)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
|
async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
|
||||||
if (!dbPromise) {
|
if (!dbPromise) {
|
||||||
@@ -101,23 +74,7 @@ async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
terminated() {
|
|
||||||
resetDBPromise()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
dbPromise
|
|
||||||
.then((db) => {
|
|
||||||
db.onversionchange = () => {
|
|
||||||
db.close()
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
db.onclose = () => {
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
resetDBPromise()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return dbPromise
|
return dbPromise
|
||||||
}
|
}
|
||||||
@@ -137,10 +94,9 @@ export function isIndexedDBAvailable(): boolean {
|
|||||||
export async function getAllTemplates(): Promise<Template[]> {
|
export async function getAllTemplates(): Promise<Template[]> {
|
||||||
if (!isIndexedDBAvailable()) return []
|
if (!isIndexedDBAvailable()) return []
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const templates = await db.getAll(STORE_NAME)
|
const templates = await db.getAll(STORE_NAME)
|
||||||
return sortTemplates(templates)
|
return sortTemplates(templates)
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get templates:", error)
|
console.error("Failed to get templates:", error)
|
||||||
return []
|
return []
|
||||||
@@ -150,9 +106,8 @@ export async function getAllTemplates(): Promise<Template[]> {
|
|||||||
export async function getTemplate(id: string): Promise<Template | null> {
|
export async function getTemplate(id: string): Promise<Template | null> {
|
||||||
if (!isIndexedDBAvailable()) return null
|
if (!isIndexedDBAvailable()) return null
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
return (await db.get(STORE_NAME, id)) || null
|
return (await db.get(STORE_NAME, id)) || null
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get template:", error)
|
console.error("Failed to get template:", error)
|
||||||
return null
|
return null
|
||||||
@@ -182,9 +137,8 @@ export async function createTemplate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.put(STORE_NAME, template)
|
await db.put(STORE_NAME, template)
|
||||||
})
|
|
||||||
return template
|
return template
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create template:", error)
|
console.error("Failed to create template:", error)
|
||||||
@@ -198,20 +152,19 @@ export async function updateTemplate(
|
|||||||
): Promise<Template | null> {
|
): Promise<Template | null> {
|
||||||
if (!isIndexedDBAvailable()) return null
|
if (!isIndexedDBAvailable()) return null
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const existing = await db.get(STORE_NAME, id)
|
const existing = await db.get(STORE_NAME, id)
|
||||||
if (!existing) return null
|
if (!existing) return null
|
||||||
|
|
||||||
const updated: Template = {
|
const updated: Template = {
|
||||||
...existing,
|
...existing,
|
||||||
...updates,
|
...updates,
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
createdAt: existing.createdAt,
|
createdAt: existing.createdAt,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}
|
}
|
||||||
await db.put(STORE_NAME, updated)
|
await db.put(STORE_NAME, updated)
|
||||||
return updated
|
return updated
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update template:", error)
|
console.error("Failed to update template:", error)
|
||||||
return null
|
return null
|
||||||
@@ -221,9 +174,8 @@ export async function updateTemplate(
|
|||||||
export async function deleteTemplate(id: string): Promise<boolean> {
|
export async function deleteTemplate(id: string): Promise<boolean> {
|
||||||
if (!isIndexedDBAvailable()) return false
|
if (!isIndexedDBAvailable()) return false
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.delete(STORE_NAME, id)
|
await db.delete(STORE_NAME, id)
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete template:", error)
|
console.error("Failed to delete template:", error)
|
||||||
@@ -237,25 +189,24 @@ export async function duplicateTemplate(
|
|||||||
): Promise<Template | null> {
|
): Promise<Template | null> {
|
||||||
if (!isIndexedDBAvailable()) return null
|
if (!isIndexedDBAvailable()) return null
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const existing = await db.get(STORE_NAME, id)
|
const existing = await db.get(STORE_NAME, id)
|
||||||
if (!existing) return null
|
if (!existing) return null
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const duplicate: Template = {
|
const duplicate: Template = {
|
||||||
...existing,
|
...existing,
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
title: `${existing.title} ${copySuffix}`,
|
title: `${existing.title} ${copySuffix}`,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
clickCount: 0,
|
clickCount: 0,
|
||||||
runCount: 0,
|
runCount: 0,
|
||||||
lastUsedAt: 0,
|
lastUsedAt: 0,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
}
|
}
|
||||||
await db.put(STORE_NAME, duplicate)
|
await db.put(STORE_NAME, duplicate)
|
||||||
return duplicate
|
return duplicate
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to duplicate template:", error)
|
console.error("Failed to duplicate template:", error)
|
||||||
return null
|
return null
|
||||||
@@ -267,13 +218,12 @@ export async function duplicateTemplate(
|
|||||||
export async function incrementClickCount(id: string): Promise<void> {
|
export async function incrementClickCount(id: string): Promise<void> {
|
||||||
if (!isIndexedDBAvailable()) return
|
if (!isIndexedDBAvailable()) return
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const template = await db.get(STORE_NAME, id)
|
const template = await db.get(STORE_NAME, id)
|
||||||
if (!template) return
|
if (!template) return
|
||||||
template.clickCount += 1
|
template.clickCount += 1
|
||||||
template.updatedAt = Date.now()
|
template.updatedAt = Date.now()
|
||||||
await db.put(STORE_NAME, template)
|
await db.put(STORE_NAME, template)
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to increment click count:", error)
|
console.error("Failed to increment click count:", error)
|
||||||
}
|
}
|
||||||
@@ -282,15 +232,14 @@ export async function incrementClickCount(id: string): Promise<void> {
|
|||||||
export async function incrementRunCount(id: string): Promise<void> {
|
export async function incrementRunCount(id: string): Promise<void> {
|
||||||
if (!isIndexedDBAvailable()) return
|
if (!isIndexedDBAvailable()) return
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const template = await db.get(STORE_NAME, id)
|
const template = await db.get(STORE_NAME, id)
|
||||||
if (!template) return
|
if (!template) return
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
template.runCount += 1
|
template.runCount += 1
|
||||||
template.lastUsedAt = now
|
template.lastUsedAt = now
|
||||||
template.updatedAt = now
|
template.updatedAt = now
|
||||||
await db.put(STORE_NAME, template)
|
await db.put(STORE_NAME, template)
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to increment run count:", error)
|
console.error("Failed to increment run count:", error)
|
||||||
}
|
}
|
||||||
@@ -423,9 +372,8 @@ export async function importTemplates(
|
|||||||
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
|
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.put(STORE_NAME, newTemplate)
|
await db.put(STORE_NAME, newTemplate)
|
||||||
})
|
|
||||||
existingKeys.add(key)
|
existingKeys.add(key)
|
||||||
imported++
|
imported++
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.14",
|
"version": "0.4.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.14",
|
"version": "0.4.15",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^4.0.1",
|
"@ai-sdk/amazon-bedrock": "^4.0.1",
|
||||||
@@ -50,8 +50,6 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
"lightningcss": "^1.32.0",
|
|
||||||
"lightningcss-linux-x64-gnu": "^1.32.0",
|
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
"nanoid": "^5.0.0",
|
"nanoid": "^5.0.0",
|
||||||
@@ -9675,9 +9673,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.9.9",
|
"version": "0.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
||||||
"integrity": "sha512-qycIHAucxy/LXAYIjmLmtQ8q9GPnMbnjG1KXhWm9o5sCr6pOYDATkMPiTNa6/v8eELyqOQ2FsEqeoFYmgv/gJg==",
|
"integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.6"
|
"node": ">=14.6"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.14",
|
"version": "0.4.15",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
|
|||||||
4
packages/mcp-server/package-lock.json
generated
4
packages/mcp-server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.17",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.17",
|
"version": "0.2.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.19",
|
"version": "0.2.0",
|
||||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -155,19 +155,22 @@ server.registerTool(
|
|||||||
server.registerTool(
|
server.registerTool(
|
||||||
"create_new_diagram",
|
"create_new_diagram",
|
||||||
{
|
{
|
||||||
description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
|
description: `Create a NEW diagram from mxGraphModel XML. ONLY use this when creating a diagram from scratch.
|
||||||
|
|
||||||
|
⚠️ DO NOT use this tool to modify an existing diagram — it will DESTROY all existing content and user changes. Use edit_diagram instead for ANY modifications to an existing diagram.
|
||||||
|
|
||||||
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
||||||
|
|
||||||
When to use this tool:
|
When to use this tool:
|
||||||
- Creating a new diagram from scratch
|
- Creating a new diagram from scratch (no existing diagram)
|
||||||
- Replacing the current diagram with a completely different one
|
- The user explicitly asks to "start over" or "create a new diagram"
|
||||||
- Major structural changes that require regenerating the diagram
|
|
||||||
|
|
||||||
When to use edit_diagram instead:
|
When to use edit_diagram instead (ALWAYS prefer edit_diagram if a diagram already exists):
|
||||||
- Small modifications to existing diagram
|
- ANY modifications to an existing diagram
|
||||||
- Adding/removing individual elements
|
- Adding/removing/moving elements
|
||||||
- Changing labels, colors, or positions
|
- Changing labels, colors, styles, or positions
|
||||||
|
- Restructuring or reorganizing existing content
|
||||||
|
- Adding new elements to an existing diagram
|
||||||
|
|
||||||
XML FORMAT - Full mxGraphModel structure:
|
XML FORMAT - Full mxGraphModel structure:
|
||||||
<mxGraphModel>
|
<mxGraphModel>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ test.describe("Iframe Interaction", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
frame
|
frame
|
||||||
.locator('text="Diagram"')
|
.locator('text="Diagram"')
|
||||||
.or(frame.locator('[title*="Diagram"]')),
|
.or(frame.locator('[title*="Diagram"]'))
|
||||||
).toBeVisible({ timeout: 10000 })
|
.filter({ visible: true })
|
||||||
|
.first(),
|
||||||
|
).toBeVisible({ timeout: 30000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("diagram XML is rendered in iframe after generation", async ({
|
test("diagram XML is rendered in iframe after generation", async ({
|
||||||
|
|||||||
@@ -208,6 +208,13 @@ describe("supportsImageInput", () => {
|
|||||||
expect(supportsImageInput("qwen3-vl-flash")).toBe(true)
|
expect(supportsImageInput("qwen3-vl-flash")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("returns true for QvQ (Qwen Visual QA) models including OpenRouter-prefixed names", () => {
|
||||||
|
expect(supportsImageInput("qvq-72b-preview")).toBe(true)
|
||||||
|
expect(supportsImageInput("qvq-max")).toBe(true)
|
||||||
|
expect(supportsImageInput("qwen/qvq-72b-preview")).toBe(true)
|
||||||
|
expect(supportsImageInput("qwen/qvq-max")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it("returns false for GLM text models", () => {
|
it("returns false for GLM text models", () => {
|
||||||
expect(supportsImageInput("glm-4")).toBe(false)
|
expect(supportsImageInput("glm-4")).toBe(false)
|
||||||
expect(supportsImageInput("glm-4-plus")).toBe(false)
|
expect(supportsImageInput("glm-4-plus")).toBe(false)
|
||||||
@@ -238,6 +245,67 @@ vi.mock("ollama-ai-provider-v2", () => {
|
|||||||
return { createOllama: mockCreateOllama, ollama: mockOllama }
|
return { createOllama: mockCreateOllama, ollama: mockOllama }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vi.mock("@ai-sdk/deepseek", () => {
|
||||||
|
const mockModel = { modelId: "test-model" }
|
||||||
|
const mockProviderFn = vi.fn(() => mockModel)
|
||||||
|
const mockCreateDeepSeek = vi.fn(() => mockProviderFn)
|
||||||
|
const mockDeepseek = vi.fn(() => mockModel)
|
||||||
|
return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
|
||||||
|
let createDeepSeekMock: ReturnType<typeof vi.fn>
|
||||||
|
const savedEnv: Record<string, string | undefined> = {}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
savedEnv.KIMI_API_KEY = process.env.KIMI_API_KEY
|
||||||
|
savedEnv.KIMI_BASE_URL = process.env.KIMI_BASE_URL
|
||||||
|
delete process.env.KIMI_BASE_URL
|
||||||
|
|
||||||
|
const mod = await import("@ai-sdk/deepseek")
|
||||||
|
createDeepSeekMock = mod.createDeepSeek as ReturnType<typeof vi.fn>
|
||||||
|
createDeepSeekMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.KIMI_API_KEY = savedEnv.KIMI_API_KEY
|
||||||
|
process.env.KIMI_BASE_URL = savedEnv.KIMI_BASE_URL
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses createDeepSeek with Kimi default base URL for reasoning_content support", () => {
|
||||||
|
process.env.KIMI_API_KEY = "test-kimi-key"
|
||||||
|
|
||||||
|
getAIModel({
|
||||||
|
provider: "kimi",
|
||||||
|
apiKey: "test-kimi-key",
|
||||||
|
modelId: "moonshot-v1-8k",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createDeepSeekMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
baseURL: "https://api.moonshot.cn/v1",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses custom base URL when provided for kimi provider", () => {
|
||||||
|
process.env.KIMI_API_KEY = "test-kimi-key"
|
||||||
|
|
||||||
|
getAIModel({
|
||||||
|
provider: "kimi",
|
||||||
|
apiKey: "test-kimi-key",
|
||||||
|
baseUrl: "https://custom-kimi-endpoint.com/v1",
|
||||||
|
modelId: "kimi-k2.6",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createDeepSeekMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
baseURL: "https://custom-kimi-endpoint.com/v1",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Ollama API key security", () => {
|
describe("Ollama API key security", () => {
|
||||||
let createOllamaMock: ReturnType<typeof vi.fn>
|
let createOllamaMock: ReturnType<typeof vi.fn>
|
||||||
const savedEnv: Record<string, string | undefined> = {}
|
const savedEnv: Record<string, string | undefined> = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user