АРЧ - UI и domain loop: синхронизировать локальную LLM-модель через shared config + КВИН 3
This commit is contained in:
@@ -153,6 +153,7 @@ export default function App() {
|
||||
});
|
||||
const presetAutoloadDoneRef = useRef(false);
|
||||
const skipPresetAutoloadRef = useRef(false);
|
||||
const sharedConnectionSyncReadyRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
@@ -192,22 +193,45 @@ export default function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
||||
if (cached) {
|
||||
try {
|
||||
const parsed = JSON.parse(cached) as Partial<ConnectionState>;
|
||||
setConnection((prev) => ({
|
||||
...prev,
|
||||
llmProvider: parsed.llmProvider === "local" ? "local" : "openai",
|
||||
model: parsed.model ?? prev.model,
|
||||
baseUrl: parsed.baseUrl ?? prev.baseUrl,
|
||||
temperature: parsed.temperature ?? prev.temperature,
|
||||
maxOutputTokens: parsed.maxOutputTokens ?? prev.maxOutputTokens
|
||||
}));
|
||||
} catch {
|
||||
// ignore broken local cache
|
||||
const bootstrapSharedConnection = async () => {
|
||||
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
||||
if (cached) {
|
||||
try {
|
||||
const parsed = JSON.parse(cached) as Partial<ConnectionState>;
|
||||
setConnection((prev) => ({
|
||||
...prev,
|
||||
llmProvider: parsed.llmProvider === "local" ? "local" : "openai",
|
||||
model: parsed.model ?? prev.model,
|
||||
baseUrl: parsed.baseUrl ?? prev.baseUrl,
|
||||
temperature: parsed.temperature ?? prev.temperature,
|
||||
maxOutputTokens: parsed.maxOutputTokens ?? prev.maxOutputTokens
|
||||
}));
|
||||
} catch {
|
||||
// ignore broken local cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await apiClient.loadSharedConnectionConfig();
|
||||
if (payload.connection && payload.connection.llmProvider === "local") {
|
||||
setConnection((prev) => ({
|
||||
...prev,
|
||||
llmProvider: "local",
|
||||
model: payload.connection?.model ?? prev.model,
|
||||
baseUrl: payload.connection?.baseUrl ?? prev.baseUrl,
|
||||
temperature: payload.connection?.temperature ?? prev.temperature,
|
||||
maxOutputTokens: payload.connection?.maxOutputTokens ?? prev.maxOutputTokens
|
||||
}));
|
||||
log(`Shared local LLM config loaded: ${payload.connection.model}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Shared local config load error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
sharedConnectionSyncReadyRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
void bootstrapSharedConnection();
|
||||
|
||||
const cachedAutorunsLayout = localStorage.getItem(AUTORUNS_LAYOUT_CONFIG_KEY);
|
||||
if (cachedAutorunsLayout) {
|
||||
@@ -304,6 +328,23 @@ export default function App() {
|
||||
void refreshRuns();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sharedConnectionSyncReadyRef.current) {
|
||||
return;
|
||||
}
|
||||
if (connection.llmProvider !== "local") {
|
||||
return;
|
||||
}
|
||||
const timer = window.setTimeout(() => {
|
||||
void apiClient
|
||||
.saveSharedConnectionConfig(connection)
|
||||
.catch((error) =>
|
||||
log(`Shared local config sync error: ${error instanceof Error ? error.message : String(error)}`)
|
||||
);
|
||||
}, 250);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [connection.baseUrl, connection.llmProvider, connection.maxOutputTokens, connection.model, connection.temperature]);
|
||||
|
||||
async function refreshHistory() {
|
||||
try {
|
||||
const payload = await apiClient.loadHistory();
|
||||
@@ -369,6 +410,17 @@ export default function App() {
|
||||
maxOutputTokens: connection.maxOutputTokens
|
||||
})
|
||||
);
|
||||
if (connection.llmProvider === "local") {
|
||||
void apiClient
|
||||
.saveSharedConnectionConfig(connection)
|
||||
.then(() => {
|
||||
log("Local config saved and synced to shared agent config (without API key).");
|
||||
})
|
||||
.catch((error) => {
|
||||
log(`Local config saved, but shared sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
log("Local config saved (without API key).");
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,32 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
async loadSharedConnectionConfig(): Promise<{
|
||||
ok: boolean;
|
||||
connection: Omit<ConnectionState, "apiKey"> | null;
|
||||
updated_at: string | null;
|
||||
exists: boolean;
|
||||
}> {
|
||||
return request("/llm/shared-connection");
|
||||
},
|
||||
|
||||
async saveSharedConnectionConfig(connection: ConnectionState): Promise<{
|
||||
ok: boolean;
|
||||
connection: Omit<ConnectionState, "apiKey">;
|
||||
updated_at: string;
|
||||
}> {
|
||||
return request("/llm/shared-connection", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
llmProvider: connection.llmProvider,
|
||||
model: connection.model,
|
||||
baseUrl: connection.baseUrl,
|
||||
temperature: connection.temperature,
|
||||
maxOutputTokens: connection.maxOutputTokens
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
async listModels(connection: ConnectionState): Promise<{ ok: boolean; models: string[]; count: number; timestamp: string }> {
|
||||
return request("/llm/models", {
|
||||
method: "POST",
|
||||
|
||||
@@ -2,6 +2,57 @@ import { useEffect, useState } from "react";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { ConnectionState } from "../state/types";
|
||||
|
||||
const LOCAL_BASE_URL = "http://127.0.0.1:1234/v1";
|
||||
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
||||
const LOCAL_QWEN25_MODEL = "qwen2.5-14b-instruct-1m";
|
||||
const LOCAL_QWEN3_MODEL = "unsloth/qwen3-30b-a3b-instruct-2507";
|
||||
|
||||
type ProviderPreset = "openai" | "local_qwen25" | "local_qwen3" | "local_custom";
|
||||
|
||||
interface ModelOptionDescriptor {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const BUILTIN_LOCAL_MODEL_OPTIONS: ModelOptionDescriptor[] = [
|
||||
{
|
||||
value: LOCAL_QWEN25_MODEL,
|
||||
label: "Qwen2.5 14B Instruct 1M"
|
||||
},
|
||||
{
|
||||
value: LOCAL_QWEN3_MODEL,
|
||||
label: "Qwen3 30B A3B Instruct 2507"
|
||||
}
|
||||
];
|
||||
|
||||
function deriveProviderPreset(value: ConnectionState): ProviderPreset {
|
||||
if (value.llmProvider !== "local") {
|
||||
return "openai";
|
||||
}
|
||||
if (value.model === LOCAL_QWEN3_MODEL) {
|
||||
return "local_qwen3";
|
||||
}
|
||||
if (value.model === LOCAL_QWEN25_MODEL) {
|
||||
return "local_qwen25";
|
||||
}
|
||||
return "local_custom";
|
||||
}
|
||||
|
||||
function buildModelOptions(modelOptions: string[], isLocal: boolean): ModelOptionDescriptor[] {
|
||||
const merged = new Map<string, ModelOptionDescriptor>();
|
||||
if (isLocal) {
|
||||
for (const option of BUILTIN_LOCAL_MODEL_OPTIONS) {
|
||||
merged.set(option.value, option);
|
||||
}
|
||||
}
|
||||
for (const modelId of modelOptions) {
|
||||
if (!merged.has(modelId)) {
|
||||
merged.set(modelId, { value: modelId, label: modelId });
|
||||
}
|
||||
}
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
|
||||
interface ConnectionPanelProps {
|
||||
value: ConnectionState;
|
||||
modelOptions: string[];
|
||||
@@ -26,7 +77,9 @@ export function ConnectionPanel({
|
||||
busy
|
||||
}: ConnectionPanelProps) {
|
||||
const isLocal = value.llmProvider === "local";
|
||||
const modelInCatalog = modelOptions.includes(value.model);
|
||||
const providerPreset = deriveProviderPreset(value);
|
||||
const availableModelOptions = buildModelOptions(modelOptions, isLocal);
|
||||
const modelInCatalog = availableModelOptions.some((option) => option.value === value.model);
|
||||
const [temperatureInput, setTemperatureInput] = useState(String(value.temperature));
|
||||
const [maxOutputTokensInput, setMaxOutputTokensInput] = useState(String(value.maxOutputTokens));
|
||||
|
||||
@@ -78,18 +131,47 @@ export function ConnectionPanel({
|
||||
<label>
|
||||
Provider
|
||||
<select
|
||||
value={value.llmProvider}
|
||||
value={providerPreset}
|
||||
onChange={(event) => {
|
||||
const nextProvider = event.target.value === "local" ? "local" : "openai";
|
||||
const selected = event.target.value as ProviderPreset;
|
||||
if (selected === "openai") {
|
||||
onChange({
|
||||
...value,
|
||||
llmProvider: "openai",
|
||||
baseUrl: OPENAI_BASE_URL
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (selected === "local_qwen25") {
|
||||
onChange({
|
||||
...value,
|
||||
llmProvider: "local",
|
||||
model: LOCAL_QWEN25_MODEL,
|
||||
baseUrl: LOCAL_BASE_URL
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (selected === "local_qwen3") {
|
||||
onChange({
|
||||
...value,
|
||||
llmProvider: "local",
|
||||
model: LOCAL_QWEN3_MODEL,
|
||||
baseUrl: LOCAL_BASE_URL
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
...value,
|
||||
llmProvider: nextProvider,
|
||||
baseUrl: nextProvider === "local" ? "http://127.0.0.1:1234/v1" : "https://api.openai.com/v1"
|
||||
llmProvider: "local",
|
||||
model: value.llmProvider === "local" ? value.model : LOCAL_QWEN25_MODEL,
|
||||
baseUrl: LOCAL_BASE_URL
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="openai">OpenAI (token)</option>
|
||||
<option value="local">Local (LM Studio / OpenAI-compatible)</option>
|
||||
<option value="local_qwen25">Qwen2.5 14B Instruct 1M (Local LM Studio)</option>
|
||||
<option value="local_qwen3">Qwen3 30B A3B Instruct 2507 (Local LM Studio)</option>
|
||||
<option value="local_custom">Local custom (LM Studio / OpenAI-compatible)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -106,20 +188,20 @@ export function ConnectionPanel({
|
||||
}}
|
||||
>
|
||||
<option value="__manual__">Manual input</option>
|
||||
{modelOptions.map((modelId) => (
|
||||
<option key={modelId} value={modelId}>
|
||||
{modelId}
|
||||
{availableModelOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Model ID (manual)
|
||||
Model ID (manual / current)
|
||||
<input
|
||||
value={value.model}
|
||||
onChange={(event) => onChange({ ...value, model: event.target.value })}
|
||||
placeholder="qwen2.5-14b-instruct or lmstudio loaded model id"
|
||||
placeholder="qwen2.5-14b-instruct-1m or unsloth/qwen3-30b-a3b-instruct-2507"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -140,7 +222,7 @@ export function ConnectionPanel({
|
||||
<input
|
||||
value={value.baseUrl}
|
||||
onChange={(event) => onChange({ ...value, baseUrl: event.target.value })}
|
||||
placeholder={isLocal ? "http://127.0.0.1:1234/v1" : "https://api.openai.com/v1"}
|
||||
placeholder={isLocal ? LOCAL_BASE_URL : OPENAI_BASE_URL}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user