Compare commits

...

5 Commits

Author SHA1 Message Date
Dayuan Jiang
d1d0de3dea chore: bump version to 0.4.7 (#416)
* chore: bump version to 0.4.7

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-25 22:30:48 +09:00
Dayuan Jiang
8c736cee0d fix: persist settings in Electron by using fixed port (#415)
- Use fixed port 61337 in production instead of random ports (10000-65535)
- localStorage is origin-specific, so random ports caused settings loss
- Add locale save/restore since language is URL-based
- Fixes #399
2025-12-25 22:20:59 +09:00
Dayuan Jiang
c5a04c9e50 feat: move delete provider button to header area (#412) 2025-12-25 19:52:07 +09:00
Dayuan Jiang
44c453403f fix: reset test button to idle state when switching providers (#411)
- Button now shows 'Test' by default instead of persisting 'Verified' state
- Verified status is still shown via green badge in provider header
- Updated OpenAI suggested models list with latest GPT-5.x series
2025-12-25 19:39:15 +09:00
Dayuan Jiang
9727aa5b39 chore: add CI workflow and Renovate configuration (#406) 2025-12-25 15:36:40 +09:00
9 changed files with 161 additions and 76 deletions

44
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npx tsc --noEmit
- name: Lint check
run: npm run check
- name: Build
run: npm run build
- name: Security audit
run: npm audit --audit-level=high --omit=dev

View File

@@ -1,4 +1,5 @@
"use client" "use client"
import { usePathname, useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { DrawIoEmbed } from "react-drawio" import { DrawIoEmbed } from "react-drawio"
import type { ImperativePanelHandle } from "react-resizable-panels" import type { ImperativePanelHandle } from "react-resizable-panels"
@@ -10,6 +11,7 @@ import {
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { i18n, type Locale } from "@/lib/i18n/config"
const drawioBaseUrl = const drawioBaseUrl =
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net" process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
@@ -24,6 +26,8 @@ export default function Home() {
showSaveDialog, showSaveDialog,
setShowSaveDialog, setShowSaveDialog,
} = useDiagram() } = useDiagram()
const router = useRouter()
const pathname = usePathname()
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<"min" | "sketch">("min")
@@ -58,6 +62,18 @@ export default function Home() {
// Load preferences from localStorage after mount // Load preferences from localStorage after mount
useEffect(() => { useEffect(() => {
// Restore saved locale and redirect if needed
const savedLocale = localStorage.getItem("next-ai-draw-io-locale")
if (savedLocale && i18n.locales.includes(savedLocale as Locale)) {
const pathParts = pathname.split("/").filter(Boolean)
const currentLocale = pathParts[0]
if (currentLocale !== savedLocale) {
pathParts[0] = savedLocale
router.replace(`/${pathParts.join("/")}`)
return // Wait for redirect
}
}
const savedUi = localStorage.getItem("drawio-theme") const savedUi = localStorage.getItem("drawio-theme")
if (savedUi === "min" || savedUi === "sketch") { if (savedUi === "min" || savedUi === "sketch") {
setDrawioUi(savedUi) setDrawioUi(savedUi)
@@ -84,7 +100,7 @@ export default function Home() {
} }
setIsLoaded(true) setIsLoaded(true)
}, []) }, [pathname, router])
const handleDarkModeChange = async () => { const handleDarkModeChange = async () => {
await saveDiagramToStorage() await saveDiagramToStorage()

View File

@@ -412,11 +412,7 @@ export function ModelConfigDialog({
setSelectedProviderId( setSelectedProviderId(
provider.id, provider.id,
) )
setValidationStatus( setValidationStatus("idle")
provider.validated
? "success"
: "idle",
)
setShowApiKey(false) setShowApiKey(false)
}} }}
className={cn( className={cn(
@@ -555,6 +551,20 @@ export function ModelConfigDialog({
</span> </span>
</div> </div>
)} )}
<Button
variant="ghost"
size="sm"
onClick={() =>
setDeleteConfirmOpen(true)
}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4 mr-1.5" />
{
dict.modelConfig
.deleteProvider
}
</Button>
</div> </div>
{/* Configuration Section */} {/* Configuration Section */}
@@ -1416,24 +1426,6 @@ export function ModelConfigDialog({
)} )}
</div> </div>
</ConfigSection> </ConfigSection>
{/* Danger Zone */}
<div className="pt-4">
<Button
variant="ghost"
size="sm"
onClick={() =>
setDeleteConfirmOpen(true)
}
className="text-muted-foreground hover:text-destructive hover:bg-destructive/5 rounded-xl"
>
<Trash2 className="h-4 w-4 mr-2" />
{
dict.modelConfig
.deleteProvider
}
</Button>
</div>
</div> </div>
</ScrollArea> </ScrollArea>
</> </>

View File

@@ -151,6 +151,9 @@ function SettingsContent({
}, [open]) }, [open])
const changeLanguage = (lang: string) => { const changeLanguage = (lang: string) => {
// Save locale to localStorage for persistence across restarts
localStorage.setItem("next-ai-draw-io-locale", lang)
const parts = pathname.split("/") const parts = pathname.split("/")
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) { if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
parts[1] = lang parts[1] = lang

View File

@@ -3,16 +3,16 @@ import { app } from "electron"
/** /**
* Port configuration * Port configuration
* Using fixed ports to preserve localStorage across restarts
* (localStorage is origin-specific, so changing ports loses all saved data)
*/ */
const PORT_CONFIG = { const PORT_CONFIG = {
// Development mode uses fixed port for hot reload compatibility // Development mode uses fixed port for hot reload compatibility
development: 6002, development: 6002,
// Production mode port range (will find first available) // Production mode uses fixed port (61337) to preserve localStorage
production: { // Falls back to sequential ports if unavailable
min: 10000, production: 61337,
max: 65535, // Maximum attempts to find an available port (fallback)
},
// Maximum attempts to find an available port
maxAttempts: 100, maxAttempts: 100,
} }
@@ -36,19 +36,11 @@ export function isPortAvailable(port: number): Promise<boolean> {
}) })
} }
/**
* Generate a random port within the production range
*/
function getRandomPort(): number {
const { min, max } = PORT_CONFIG.production
return Math.floor(Math.random() * (max - min + 1)) + min
}
/** /**
* Find an available port * Find an available port
* - In development: uses fixed port (6002) * - In development: uses fixed port (6002)
* - In production: finds a random available port * - In production: uses fixed port (61337) to preserve localStorage
* - If a port was previously allocated, verifies it's still available * - Falls back to sequential ports if preferred port is unavailable
* *
* @param reuseExisting If true, try to reuse the previously allocated port * @param reuseExisting If true, try to reuse the previously allocated port
* @returns Promise<number> The available port * @returns Promise<number> The available port
@@ -56,6 +48,9 @@ function getRandomPort(): number {
*/ */
export async function findAvailablePort(reuseExisting = true): Promise<number> { export async function findAvailablePort(reuseExisting = true): Promise<number> {
const isDev = !app.isPackaged const isDev = !app.isPackaged
const preferredPort = isDev
? PORT_CONFIG.development
: PORT_CONFIG.production
// Try to reuse cached port if requested and available // Try to reuse cached port if requested and available
if (reuseExisting && allocatedPort !== null) { if (reuseExisting && allocatedPort !== null) {
@@ -69,29 +64,22 @@ export async function findAvailablePort(reuseExisting = true): Promise<number> {
allocatedPort = null allocatedPort = null
} }
if (isDev) { // Try preferred port first
// Development mode: use fixed port if (await isPortAvailable(preferredPort)) {
const port = PORT_CONFIG.development allocatedPort = preferredPort
const available = await isPortAvailable(port) return preferredPort
if (available) {
allocatedPort = port
return port
}
console.warn(
`Development port ${port} is in use, finding alternative...`,
)
} }
// Production mode or dev port unavailable: find random available port console.warn(
for (let attempt = 0; attempt < PORT_CONFIG.maxAttempts; attempt++) { `Preferred port ${preferredPort} is in use, finding alternative...`,
const port = isDev )
? PORT_CONFIG.development + attempt + 1
: getRandomPort()
const available = await isPortAvailable(port) // Fallback: try sequential ports starting from preferred + 1
if (available) { for (let attempt = 1; attempt <= PORT_CONFIG.maxAttempts; attempt++) {
const port = preferredPort + attempt
if (await isPortAvailable(port)) {
allocatedPort = port allocatedPort = port
console.log(`Allocated port: ${port}`) console.log(`Allocated fallback port: ${port}`)
return port return port
} }
} }

View File

@@ -83,22 +83,24 @@ export const PROVIDER_INFO: Record<
// Suggested models per provider for quick add // Suggested models per provider for quick add
export const SUGGESTED_MODELS: Record<ProviderName, string[]> = { export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
openai: [ openai: [
// GPT-4o series (latest) "gpt-5.2-pro",
"gpt-5.2-chat-latest",
"gpt-5.2",
"gpt-5.1-codex-mini",
"gpt-5.1-codex",
"gpt-5.1-chat-latest",
"gpt-5.1",
"gpt-5-pro",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-5-codex",
"gpt-5-chat-latest",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o", "gpt-4o",
"gpt-4o-mini", "gpt-4o-mini",
"gpt-4o-2024-11-20",
// GPT-4 Turbo
"gpt-4-turbo",
"gpt-4-turbo-preview",
// o1/o3 reasoning models
"o1",
"o1-mini",
"o1-preview",
"o3-mini",
// GPT-4
"gpt-4",
// GPT-3.5
"gpt-3.5-turbo",
], ],
anthropic: [ anthropic: [
// Claude 4.5 series (latest) // Claude 4.5 series (latest)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.6", "version": "0.4.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.6", "version": "0.4.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.1", "@ai-sdk/amazon-bedrock": "^4.0.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.6", "version": "0.4.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",

40
renovate.json Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["after 10am on saturday"],
"timezone": "Asia/Tokyo",
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchPackagePatterns": ["*"],
"groupName": "minor and patch dependencies",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"matchPackagePatterns": ["*"],
"automerge": false
},
{
"matchPackagePatterns": ["@ai-sdk/*"],
"groupName": "AI SDK packages"
},
{
"matchPackagePatterns": ["@radix-ui/*"],
"groupName": "Radix UI packages"
},
{
"matchPackagePatterns": ["electron", "electron-builder"],
"groupName": "Electron packages",
"automerge": false
},
{
"matchPackagePatterns": ["@ai-sdk/*", "ai", "next"],
"groupName": "Core framework packages",
"automerge": false
}
],
"vulnerabilityAlerts": {
"enabled": true
}
}