canvas-website/src/ui/SettingsDialog.tsx

356 lines
12 KiB
TypeScript

import {
TLUiDialogProps,
TldrawUiButton,
TldrawUiButtonLabel,
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
TldrawUiInput,
} from "tldraw"
import React from "react"
import { PROVIDERS, AI_PERSONALITIES, OLLAMA_MODELS } from "../lib/settings"
import { useAuth } from "../context/AuthContext"
import { getOllamaConfig } from "../lib/clientConfig"
export function SettingsDialog({ onClose }: TLUiDialogProps) {
const { session } = useAuth()
const [apiKeys, setApiKeys] = React.useState(() => {
try {
// First try to get user-specific API keys if logged in
if (session.authed && session.username) {
const userApiKeys = localStorage.getItem(`${session.username}_api_keys`)
if (userApiKeys) {
try {
const parsed = JSON.parse(userApiKeys)
if (parsed.keys) {
return parsed.keys
}
} catch (e) {
// Continue to fallback
}
}
}
// Fallback to global API keys
const stored = localStorage.getItem("openai_api_key")
if (stored) {
try {
const parsed = JSON.parse(stored)
if (parsed.keys) {
return parsed.keys
}
} catch (e) {
// Fallback to old format
return { openai: stored }
}
}
return { openai: '', anthropic: '', google: '' }
} catch (e) {
return { openai: '', anthropic: '', google: '' }
}
})
const [personality, setPersonality] = React.useState(() => {
try {
// First try to get user-specific settings if logged in
if (session.authed && session.username) {
const userApiKeys = localStorage.getItem(`${session.username}_api_keys`)
if (userApiKeys) {
try {
const parsed = JSON.parse(userApiKeys)
if (parsed.personality) {
return parsed.personality
}
} catch (e) {
// Continue to fallback
}
}
}
// Fallback to global settings
const stored = localStorage.getItem("openai_api_key")
if (stored) {
try {
const parsed = JSON.parse(stored)
if (parsed.personality) {
return parsed.personality
}
} catch (e) {
// Continue to fallback
}
}
return 'web-developer'
} catch (e) {
return 'web-developer'
}
})
const [ollamaModel, setOllamaModel] = React.useState(() => {
try {
// First try to get user-specific settings if logged in
if (session.authed && session.username) {
const userApiKeys = localStorage.getItem(`${session.username}_api_keys`)
if (userApiKeys) {
try {
const parsed = JSON.parse(userApiKeys)
if (parsed.ollamaModel) {
return parsed.ollamaModel
}
} catch (e) {
// Continue to fallback
}
}
}
// Fallback to global settings
const stored = localStorage.getItem("openai_api_key")
if (stored) {
try {
const parsed = JSON.parse(stored)
if (parsed.ollamaModel) {
return parsed.ollamaModel
}
} catch (e) {
// Continue to fallback
}
}
return 'llama3.1:8b'
} catch (e) {
return 'llama3.1:8b'
}
})
// Check if Ollama is configured
const ollamaConfig = getOllamaConfig()
const handleKeyChange = (provider: string, value: string) => {
const newKeys = { ...apiKeys, [provider]: value }
setApiKeys(newKeys)
saveSettings(newKeys, personality, ollamaModel)
}
const handlePersonalityChange = (newPersonality: string) => {
setPersonality(newPersonality)
saveSettings(apiKeys, newPersonality, ollamaModel)
}
const handleOllamaModelChange = (newModel: string) => {
setOllamaModel(newModel)
saveSettings(apiKeys, personality, newModel)
}
const saveSettings = (keys: any, personalityValue: string, ollamaModelValue: string) => {
// Save to localStorage with the new structure
const settings = {
keys: keys,
provider: 'openai', // Default provider
models: Object.fromEntries(PROVIDERS.map((provider) => [provider.id, provider.models[0]])),
ollamaModel: ollamaModelValue,
personality: personalityValue,
}
// If user is logged in, save to user-specific storage
if (session.authed && session.username) {
console.log(`💾 Saving user-specific settings for ${session.username}:`, settings);
localStorage.setItem(`${session.username}_api_keys`, JSON.stringify(settings))
// Also save to global storage as fallback
localStorage.setItem("openai_api_key", JSON.stringify(settings))
} else {
console.log("💾 Saving global settings to localStorage:", settings);
localStorage.setItem("openai_api_key", JSON.stringify(settings))
}
}
const validateKey = (provider: string, key: string) => {
const providerConfig = PROVIDERS.find(p => p.id === provider)
if (providerConfig && key.trim()) {
return providerConfig.validate(key)
}
return true
}
return (
<>
<TldrawUiDialogHeader>
<TldrawUiDialogTitle>AI Settings</TldrawUiDialogTitle>
<TldrawUiDialogCloseButton />
</TldrawUiDialogHeader>
<TldrawUiDialogBody style={{ maxWidth: 400 }}>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{/* AI Personality Selector */}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<label style={{ fontWeight: "500", fontSize: "14px" }}>
AI Personality
</label>
<select
value={personality}
onChange={(e) => handlePersonalityChange(e.target.value)}
style={{
padding: "8px 12px",
border: "1px solid #d1d5db",
borderRadius: "6px",
fontSize: "14px",
backgroundColor: "white",
cursor: "pointer"
}}
>
{AI_PERSONALITIES.map((personality) => (
<option key={personality.id} value={personality.id}>
{personality.name} - {personality.description}
</option>
))}
</select>
</div>
{/* Ollama Model Selector - Only show if Ollama is configured */}
{ollamaConfig && (
<div style={{ borderTop: "1px solid #e5e7eb", paddingTop: "16px" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: "12px" }}>
<span style={{ fontSize: "20px" }}>🦙</span>
<h3 style={{ fontSize: "16px", fontWeight: "600", margin: 0 }}>
Private AI Model
</h3>
<span style={{
fontSize: "11px",
color: "#059669",
backgroundColor: "#d1fae5",
padding: "2px 8px",
borderRadius: "9999px",
fontWeight: "500"
}}>
FREE
</span>
</div>
<p style={{
fontSize: "12px",
color: "#6b7280",
marginBottom: "12px",
lineHeight: "1.4"
}}>
Running on your private server. No API key needed - select quality vs speed.
</p>
<select
value={ollamaModel}
onChange={(e) => handleOllamaModelChange(e.target.value)}
style={{
width: "100%",
padding: "10px 12px",
border: "1px solid #d1d5db",
borderRadius: "6px",
fontSize: "14px",
backgroundColor: "white",
cursor: "pointer"
}}
>
{OLLAMA_MODELS.map((model) => (
<option key={model.id} value={model.id}>
{model.name} - {model.description}
</option>
))}
</select>
<div style={{
display: "flex",
justifyContent: "space-between",
marginTop: "8px",
fontSize: "11px",
color: "#9ca3af"
}}>
<span>Server: {ollamaConfig.url}</span>
<span>
Model size: {OLLAMA_MODELS.find(m => m.id === ollamaModel)?.size || 'Unknown'}
</span>
</div>
</div>
)}
{/* API Keys Section */}
<div style={{ borderTop: "1px solid #e5e7eb", paddingTop: "16px" }}>
<h3 style={{ fontSize: "16px", fontWeight: "600", marginBottom: "8px" }}>
Cloud API Keys
</h3>
<p style={{
fontSize: "12px",
color: "#6b7280",
marginBottom: "16px",
lineHeight: "1.4"
}}>
{ollamaConfig
? "Optional fallback - used when private AI is unavailable."
: "Enter API keys to use cloud AI services."}
</p>
{PROVIDERS.map((provider) => (
<div key={provider.id} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<label style={{ fontWeight: "500", fontSize: "14px" }}>
{provider.name} API Key
</label>
<span style={{
fontSize: "12px",
color: "#666",
backgroundColor: "#f3f4f6",
padding: "2px 6px",
borderRadius: "4px"
}}>
{provider.models[0]}
</span>
</div>
<TldrawUiInput
value={apiKeys[provider.id] || ''}
placeholder={`Enter your ${provider.name} API key`}
onValueChange={(value) => handleKeyChange(provider.id, value)}
/>
{apiKeys[provider.id] && !validateKey(provider.id, apiKeys[provider.id]) && (
<div style={{
fontSize: "12px",
color: "#ef4444",
marginTop: "4px"
}}>
Invalid API key format
</div>
)}
<div style={{
fontSize: "11px",
color: "#666",
lineHeight: "1.4"
}}>
{provider.help && (
<a
href={provider.help}
target="_blank"
rel="noopener noreferrer"
style={{ color: "#3b82f6", textDecoration: "none" }}
>
Learn more about {provider.name} setup
</a>
)}
</div>
</div>
))}
</div> {/* Close API Keys Section */}
<div style={{
padding: "12px",
backgroundColor: "#f8fafc",
borderRadius: "6px",
border: "1px solid #e2e8f0"
}}>
<div style={{ fontSize: "12px", color: "#475569", lineHeight: "1.4" }}>
<strong>Note:</strong> API keys are stored locally in your browser.
Make sure to use keys with appropriate usage limits for your needs.
</div>
</div>
</div>
</TldrawUiDialogBody>
<TldrawUiDialogFooter>
<TldrawUiButton type="primary" onClick={onClose}>
<TldrawUiButtonLabel>Close</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDialogFooter>
</>
)
}