Skip to content

Commit 2394311

Browse files
committed
feat: database storage & ai api key model (#5)
1 parent c60eff8 commit 2394311

File tree

19 files changed

+726
-22
lines changed

19 files changed

+726
-22
lines changed

binding/database/service.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package database
2+
3+
import (
4+
db "firebringer/database"
5+
)
6+
7+
// Service provides database methods for the frontend
8+
type Service struct{}
9+
10+
// NewService creates a new Database Service
11+
func NewService() *Service {
12+
return &Service{}
13+
}
14+
15+
// GetAIConfig returns the configuration for a specific provider
16+
func (s *Service) GetAIConfig(provider string) (*db.AIConfig, error) {
17+
return db.GetConfig(db.AIProvider(provider))
18+
}
19+
20+
// SaveAIConfig saves the configuration for a specific provider
21+
func (s *Service) SaveAIConfig(config db.AIConfig) error {
22+
return db.SaveConfig(&config)
23+
}
24+
25+
// DeleteAIConfig deletes the configuration for a specific provider
26+
func (s *Service) DeleteAIConfig(provider string) error {
27+
return db.DeleteConfig(db.AIProvider(provider))
28+
}
29+
30+
// ListAIConfigs returns all AI configurations
31+
func (s *Service) ListAIConfigs() ([]db.AIConfig, error) {
32+
return db.ListConfigs()
33+
}

database/db.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package database
2+
3+
import (
4+
"log"
5+
6+
"firebringer/storage"
7+
8+
"github.com/jmoiron/sqlx"
9+
_ "github.com/mattn/go-sqlite3"
10+
)
11+
12+
var DB *sqlx.DB
13+
14+
func InitDB() error {
15+
dbPath, err := storage.GetDatabasePath()
16+
if err != nil {
17+
return err
18+
}
19+
log.Printf("Database path: %s", dbPath)
20+
21+
DB, err = sqlx.Connect("sqlite3", dbPath)
22+
if err != nil {
23+
return err
24+
}
25+
26+
schema := `
27+
CREATE TABLE IF NOT EXISTS ai_configs (
28+
provider TEXT PRIMARY KEY,
29+
api_key TEXT NOT NULL,
30+
base_url TEXT DEFAULT '',
31+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
32+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
33+
);
34+
`
35+
_, err = DB.Exec(schema)
36+
if err != nil {
37+
return err
38+
}
39+
40+
log.Println("Database initialized successfully")
41+
return nil
42+
}

database/models.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package database
2+
3+
import "time"
4+
5+
type AIProvider string
6+
7+
const (
8+
ProviderGemini AIProvider = "gemini"
9+
ProviderOpenAI AIProvider = "openai"
10+
ProviderClaude AIProvider = "claude"
11+
)
12+
13+
type AIConfig struct {
14+
Provider AIProvider `db:"provider" json:"provider"`
15+
APIKey string `db:"api_key" json:"apiKey"`
16+
BaseURL string `db:"base_url" json:"baseUrl"`
17+
CreatedAt time.Time `db:"created_at" json:"createdAt"`
18+
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
19+
}

database/repository.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package database
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"log"
7+
)
8+
9+
func GetConfig(provider AIProvider) (*AIConfig, error) {
10+
config := AIConfig{}
11+
err := DB.Get(&config, "SELECT * FROM ai_configs WHERE provider = ?", provider)
12+
if err != nil {
13+
if err == sql.ErrNoRows {
14+
return nil, nil
15+
}
16+
return nil, err
17+
}
18+
return &config, nil
19+
}
20+
21+
func SaveConfig(config *AIConfig) error {
22+
log.Println("Saving config:", config)
23+
if config.Provider == "" {
24+
return fmt.Errorf("provider is required")
25+
}
26+
27+
// Upsert logic for SQLite
28+
query := `
29+
INSERT INTO ai_configs (provider, api_key, base_url, updated_at)
30+
VALUES (:provider, :api_key, :base_url, CURRENT_TIMESTAMP)
31+
ON CONFLICT(provider) DO UPDATE SET
32+
api_key = excluded.api_key,
33+
base_url = excluded.base_url,
34+
updated_at = CURRENT_TIMESTAMP;
35+
`
36+
_, err := DB.NamedExec(query, config)
37+
return err
38+
}
39+
40+
func DeleteConfig(provider AIProvider) error {
41+
_, err := DB.Exec("DELETE FROM ai_configs WHERE provider = ?", provider)
42+
return err
43+
}
44+
45+
func ListConfigs() ([]AIConfig, error) {
46+
configs := []AIConfig{}
47+
err := DB.Select(&configs, "SELECT * FROM ai_configs ORDER BY created_at DESC")
48+
return configs, err
49+
}

frontend/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
22
import { ProjectList } from "@/components/project-list";
33
import { CanvasView } from "@/components/canvas-view";
44
import { AppSidebar } from "@/components/app-sidebar";
5+
import { SettingsDialog } from "@/components/settings/settings-dialog";
56
import { useState } from "react";
67

78
interface Project {
@@ -12,6 +13,7 @@ interface Project {
1213

1314
export default function App() {
1415
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
16+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
1517

1618
const handleProjectClick = (project: Project) => {
1719
setSelectedProject(project);
@@ -40,11 +42,12 @@ export default function App() {
4042
style={{ "--wails-draggable": "drag" } as React.CSSProperties}
4143
></div>
4244
<SidebarProvider>
43-
<AppSidebar />
45+
<AppSidebar onSettingsClick={() => setIsSettingsOpen(true)} />
4446
<SidebarInset className="m-2 rounded-2xl z-10 border">
4547
<ProjectList onProjectClick={handleProjectClick} />
4648
</SidebarInset>
4749
</SidebarProvider>
50+
<SettingsDialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen} />
4851
</div>
4952
);
5053
}

frontend/src/components/app-sidebar.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
SidebarMenuItem,
1212
} from "@/components/ui/sidebar";
1313
import { NavUser } from "@/components/nav-user";
14-
import { Home, Image, Settings } from "lucide-react";
14+
import { Home, Image, Settings, Telescope } from "lucide-react";
1515

16-
export function AppSidebar() {
16+
export function AppSidebar({ onSettingsClick }: { onSettingsClick: () => void }) {
1717
return (
1818
<Sidebar className="border-none">
1919
<SidebarHeader>
@@ -39,22 +39,30 @@ export function AppSidebar() {
3939
</SidebarMenuItem>
4040
<SidebarMenuItem>
4141
<SidebarMenuButton>
42-
<Settings />
43-
<span>设置</span>
42+
<Telescope />
43+
<span>浏览</span>
4444
</SidebarMenuButton>
4545
</SidebarMenuItem>
4646
</SidebarMenu>
4747
</SidebarGroupContent>
4848
</SidebarGroup>
4949
</SidebarContent>
5050
<SidebarFooter>
51-
<NavUser
51+
<SidebarMenu>
52+
<SidebarMenuItem>
53+
<SidebarMenuButton onClick={onSettingsClick}>
54+
<Settings />
55+
<span>设置</span>
56+
</SidebarMenuButton>
57+
</SidebarMenuItem>
58+
</SidebarMenu>
59+
{/* <NavUser
5260
user={{
5361
name: "用户名",
5462
email: "user@example.com",
5563
avatar: "https://placehold.co/100x100/6366f1/ffffff?text=U",
5664
}}
57-
/>
65+
/> */}
5866
</SidebarFooter>
5967
</Sidebar>
6068
);
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useEffect, useState } from "react";
2+
import { Button } from "@/components/ui/button";
3+
import { Input } from "@/components/ui/input";
4+
import { Label } from "@/components/ui/label";
5+
import { Separator } from "@/components/ui/separator";
6+
import { GetAIConfig, SaveAIConfig } from "../../../wailsjs/go/database/Service";
7+
import { database } from "../../../wailsjs/go/models";
8+
9+
const PROVIDERS = ["gemini", "openai", "claude"];
10+
11+
export function AIModelSettings() {
12+
const [configs, setConfigs] = useState<Record<string, database.AIConfig>>({});
13+
const [loading, setLoading] = useState(false);
14+
15+
useEffect(() => {
16+
loadConfigs();
17+
}, []);
18+
19+
const loadConfigs = async () => {
20+
setLoading(true);
21+
const newConfigs: Record<string, database.AIConfig> = {};
22+
try {
23+
for (const provider of PROVIDERS) {
24+
const config = await GetAIConfig(provider);
25+
newConfigs[provider] = config || new database.AIConfig({ provider });
26+
}
27+
setConfigs(newConfigs);
28+
} catch (err) {
29+
console.error("Failed to load configs", err);
30+
} finally {
31+
setLoading(false);
32+
}
33+
};
34+
35+
const handleSave = async (provider: string) => {
36+
const config = configs[provider];
37+
if (!config) return;
38+
try {
39+
config.provider = provider;
40+
await SaveAIConfig(config);
41+
// Ideally show a toast
42+
} catch (err) {
43+
console.error(`Failed to save ${provider}`, err);
44+
}
45+
};
46+
47+
const handleChange = (provider: string, field: "apiKey" | "baseUrl", value: string) => {
48+
setConfigs((prev) => ({
49+
...prev,
50+
[provider]: new database.AIConfig({
51+
...prev[provider],
52+
[field]: value,
53+
}),
54+
}));
55+
};
56+
57+
return (
58+
<div className="space-y-8">
59+
{PROVIDERS.map((provider, index) => {
60+
const config = configs[provider] || new database.AIConfig({ provider });
61+
62+
return (
63+
<div key={provider} className="space-y-4">
64+
{index > 0 && <Separator className="my-6" />}
65+
66+
<div className="flex items-center justify-between">
67+
<div>
68+
<h4 className="text-base font-medium capitalize">{provider}</h4>
69+
<p className="text-sm text-muted-foreground">
70+
Configure connection details for {provider}.
71+
</p>
72+
</div>
73+
<Button
74+
variant="outline"
75+
size="sm"
76+
onClick={() => handleSave(provider)}
77+
disabled={loading}
78+
>
79+
保存
80+
</Button>
81+
</div>
82+
83+
<div className="grid gap-4 bg-muted/30 p-4 rounded-lg border">
84+
<div className="grid gap-2">
85+
<Label htmlFor={`${provider}-key`}>API Key</Label>
86+
<Input
87+
id={`${provider}-key`}
88+
type="password"
89+
className="bg-background"
90+
value={config.apiKey || ""}
91+
onChange={(e) => handleChange(provider, "apiKey", e.target.value)}
92+
placeholder={`sk-...`}
93+
/>
94+
</div>
95+
<div className="grid gap-2">
96+
<Label htmlFor={`${provider}-base`}>Base URL</Label>
97+
<Input
98+
id={`${provider}-base`}
99+
className="bg-background"
100+
value={config.baseUrl || ""}
101+
onChange={(e) => handleChange(provider, "baseUrl", e.target.value)}
102+
placeholder="Default"
103+
/>
104+
<p className="text-[0.8rem] text-muted-foreground">
105+
Optional. Leave empty to use the default endpoint.
106+
</p>
107+
</div>
108+
</div>
109+
</div>
110+
);
111+
})}
112+
</div>
113+
);
114+
}

0 commit comments

Comments
 (0)