can change openrouter model in UI
This commit is contained in:
@@ -4,11 +4,13 @@
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Job, JobSource, JobStatus } from "../shared/types";
|
||||
import { Header, JobList, PipelineProgress, Stats } from "./components";
|
||||
import * as api from "./api";
|
||||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
|
||||
const DEFAULT_PIPELINE_SOURCES: JobSource[] = ["gradcracker", "indeed", "linkedin"];
|
||||
const PIPELINE_SOURCES_STORAGE_KEY = "jobops.pipeline.sources";
|
||||
@@ -170,17 +172,25 @@ export const App: React.FC = () => {
|
||||
onPipelineSourcesChange={setPipelineSources}
|
||||
/>
|
||||
|
||||
<main className="container mx-auto max-w-7xl space-y-6 px-4 py-6 pb-12">
|
||||
<PipelineProgress isRunning={isPipelineRunning} />
|
||||
<Stats stats={stats} />
|
||||
<JobList
|
||||
jobs={jobs}
|
||||
onApply={handleApply}
|
||||
onReject={handleReject}
|
||||
onProcess={handleProcess}
|
||||
processingJobId={processingJobId}
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<main className="container mx-auto max-w-7xl space-y-6 px-4 py-6 pb-12">
|
||||
<PipelineProgress isRunning={isPipelineRunning} />
|
||||
<Stats stats={stats} />
|
||||
<JobList
|
||||
jobs={jobs}
|
||||
onApply={handleApply}
|
||||
onReject={handleReject}
|
||||
onProcess={handleProcess}
|
||||
processingJobId={processingJobId}
|
||||
/>
|
||||
</main>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
|
||||
<Toaster position="bottom-right" richColors closeButton />
|
||||
</>
|
||||
|
||||
@@ -8,7 +8,8 @@ import type {
|
||||
JobsListResponse,
|
||||
PipelineStatusResponse,
|
||||
JobSource,
|
||||
PipelineRun
|
||||
PipelineRun,
|
||||
AppSettings,
|
||||
} from '../../shared/types';
|
||||
|
||||
const API_BASE = '/api';
|
||||
@@ -92,6 +93,18 @@ export async function runPipeline(config?: {
|
||||
});
|
||||
}
|
||||
|
||||
// Settings API
|
||||
export async function getSettings(): Promise<AppSettings> {
|
||||
return fetchApi<AppSettings>('/settings');
|
||||
}
|
||||
|
||||
export async function updateSettings(update: { model?: string | null }): Promise<AppSettings> {
|
||||
return fetchApi<AppSettings>('/settings', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(update),
|
||||
});
|
||||
}
|
||||
|
||||
// Database API
|
||||
export async function clearDatabase(): Promise<{
|
||||
message: string;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { ChevronDown, Loader2, Play, RefreshCcw, Rocket, Trash2 } from "lucide-react";
|
||||
import { ChevronDown, Loader2, Play, RefreshCcw, Rocket, Settings, Trash2 } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -112,6 +113,13 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<span className="hidden sm:inline">Refresh</span>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link to="/settings">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Settings</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onRunPipeline}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { App } from './App';
|
||||
import '../index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Settings page.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import type { AppSettings } from "../../shared/types"
|
||||
import * as api from "../api"
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const [settings, setSettings] = useState<AppSettings | null>(null)
|
||||
const [modelDraft, setModelDraft] = useState("")
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
setIsLoading(true)
|
||||
api
|
||||
.getSettings()
|
||||
.then((data) => {
|
||||
if (!isMounted) return
|
||||
setSettings(data)
|
||||
setModelDraft(data.overrideModel ?? "")
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : "Failed to load settings"
|
||||
toast.error(message)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!isMounted) return
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const effectiveModel = settings?.model ?? ""
|
||||
const defaultModel = settings?.defaultModel ?? ""
|
||||
const overrideModel = settings?.overrideModel
|
||||
|
||||
const canSave = useMemo(() => {
|
||||
if (!settings) return false
|
||||
const next = modelDraft.trim()
|
||||
const current = (overrideModel ?? "").trim()
|
||||
return next !== current
|
||||
}, [modelDraft, overrideModel, settings])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!settings) return
|
||||
try {
|
||||
setIsSaving(true)
|
||||
const trimmed = modelDraft.trim()
|
||||
const updated = await api.updateSettings({ model: trimmed.length > 0 ? trimmed : null })
|
||||
setSettings(updated)
|
||||
setModelDraft(updated.overrideModel ?? "")
|
||||
toast.success("Settings saved")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to save settings"
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
setIsSaving(true)
|
||||
const updated = await api.updateSettings({ model: null })
|
||||
setSettings(updated)
|
||||
setModelDraft("")
|
||||
toast.success("Reset to default")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to reset settings"
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container mx-auto max-w-3xl space-y-6 px-4 py-6 pb-12">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-xl font-semibold tracking-tight">Settings</h1>
|
||||
<p className="text-sm text-muted-foreground">Configure runtime behavior for this app.</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Model</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Override model</div>
|
||||
<Input
|
||||
value={modelDraft}
|
||||
onChange={(event) => setModelDraft(event.target.value)}
|
||||
placeholder={defaultModel || "openai/gpt-4o-mini"}
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Leave blank to use the default from server env (`MODEL`).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid gap-2 text-sm sm:grid-cols-2">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Effective</div>
|
||||
<div className="break-words font-mono text-xs">{effectiveModel || "—"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Default (env)</div>
|
||||
<div className="break-words font-mono text-xs">{defaultModel || "—"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={handleSave} disabled={isLoading || isSaving || !canSave}>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset} disabled={isLoading || isSaving || !settings}>
|
||||
Reset to default
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user