АРЧ - UI и domain loop: синхронизировать локальную LLM-модель через shared config + КВИН 3

This commit is contained in:
2026-04-15 09:15:35 +03:00
parent bc381c012e
commit 8866176be6
24 changed files with 801 additions and 73 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-B9mz4jx4.js"></script>
<script type="module" crossorigin src="/assets/index-Qq5WpuqR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dcuz1nX5.css">
</head>
<body>
+67 -15
View File
@@ -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).");
}
+26
View File
@@ -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>