mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
Compare commits
13 Commits
fix/biome-
...
fix/parse-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9656fc0cfe | ||
|
|
1115b2d2cd | ||
|
|
08afb6dd34 | ||
|
|
c703159e00 | ||
|
|
ee75408136 | ||
|
|
2ed1a64ae3 | ||
|
|
2f2d75961d | ||
|
|
5406778dd6 | ||
|
|
bb65a8c07a | ||
|
|
4e223b6237 | ||
|
|
c60e3930a3 | ||
|
|
5c8ae4d6d7 | ||
|
|
f965f3fa2e |
@@ -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,7 +1,7 @@
|
|||||||
import { extract } from "@extractus/article-extractor"
|
import { extract } from "@extractus/article-extractor"
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import TurndownService from "turndown"
|
import TurndownService from "turndown"
|
||||||
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
|
import { isPrivateUrl } from "@/lib/ssrf-protection"
|
||||||
|
|
||||||
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
||||||
const EXTRACT_TIMEOUT_MS = 15000
|
const EXTRACT_TIMEOUT_MS = 15000
|
||||||
@@ -28,8 +28,10 @@ export async function POST(req: Request) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSRF protection
|
// SSRF protection: parse-url has no use case for fetching internal
|
||||||
if (!allowPrivateUrls && isPrivateUrl(url)) {
|
// hosts, so private URLs are always rejected. ALLOW_PRIVATE_URLS only
|
||||||
|
// governs LLM provider baseUrl overrides (validate-model, chat).
|
||||||
|
if (isPrivateUrl(url)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Cannot access private/internal URLs" },
|
{ error: "Cannot access private/internal URLs" },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
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`,
|
||||||
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
export function isPrivateUrl(urlString: string): boolean {
|
export function isPrivateUrl(urlString: string): boolean {
|
||||||
try {
|
try {
|
||||||
const url = new URL(urlString)
|
const url = new URL(urlString)
|
||||||
const hostname = url.hostname.toLowerCase()
|
// Strip a trailing dot so FQDN forms like "localhost." (which still
|
||||||
|
// resolve to 127.0.0.1) cannot bypass the equality checks below.
|
||||||
|
const hostname = url.hostname.toLowerCase().replace(/\.$/, "")
|
||||||
|
|
||||||
// Block localhost
|
// Block localhost
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
2466
package-lock.json
generated
2466
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.15",
|
"version": "0.4.16",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
"@langfuse/tracing": "^4.4.9",
|
"@langfuse/tracing": "^4.4.9",
|
||||||
"@next/third-parties": "^16.0.6",
|
"@next/third-parties": "^16.0.6",
|
||||||
"@opennextjs/cloudflare": "^1.17.1",
|
"@opennextjs/cloudflare": "^1.17.1",
|
||||||
"@openrouter/ai-sdk-provider": "^1.5.4",
|
"@openrouter/ai-sdk-provider": "^2.0.0",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"nanoid": "^5.0.0",
|
"nanoid": "^5.0.0",
|
||||||
"negotiator": "^1.0.0",
|
"negotiator": "^1.0.0",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"ollama-ai-provider-v2": "^2.0.0",
|
"ollama-ai-provider-v2": "^3.0.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"react": "^19.1.2",
|
"react": "^19.1.2",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||||
"@biomejs/biome": "2.4.10",
|
"@biomejs/biome": "2.4.13",
|
||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "9.39.4",
|
"eslint": "9.39.4",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|||||||
12
packages/mcp-server/package-lock.json
generated
12
packages/mcp-server/package-lock.json
generated
@@ -521,9 +521,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.12.0",
|
"version": "24.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
||||||
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
|
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2062,9 +2062,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -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 ({
|
||||||
|
|||||||
@@ -245,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