Nytt CLI-verktøy `synops-ai` som leser cli_tool-noder fra PG, bygger en systemprompt med tilgjengelige verktøy og script-grammatikk, og bruker LLM til å foreslå orkestreringsscript fra fritekst-beskrivelse. Tre moduser: - Synkron: --description "..." → LLM genererer script → JSON output - System prompt: --generate-system-prompt → skriver auto-generert prompt - Eventually: --eventually → lagrer som work_item for Claude Code Maskinrommet: nytt endepunkt POST /intentions/ai_suggest_script som kaller synops-ai, validerer resultatet med script_compiler, og returnerer script + kompileringsresultat til frontend. Frontend: AI-assistent-knapp i OrchestrationTrait med fritekst-input, generer-knapp, og feilvisning. Generert script settes direkte i editoren. Migration: synops-ai seeded som cli_tool-node med norske verb-alias.
1127 lines
26 KiB
Svelte
1127 lines
26 KiB
Svelte
<script lang="ts">
|
|
import type { Node } from '$lib/realtime';
|
|
import { edgeStore, nodeStore, nodeVisibility } from '$lib/realtime';
|
|
import {
|
|
updateNode,
|
|
compileScript,
|
|
testOrchestration,
|
|
fetchOrchestrationLog,
|
|
aiSuggestScript,
|
|
type CompileScriptResponse,
|
|
type OrchestrationLogEntry
|
|
} from '$lib/api';
|
|
|
|
interface Props {
|
|
collection?: Node;
|
|
config: Record<string, unknown>;
|
|
userId?: string;
|
|
accessToken?: string;
|
|
}
|
|
|
|
let { collection, config, userId, accessToken }: Props = $props();
|
|
|
|
// =========================================================================
|
|
// Orchestration nodes in this collection
|
|
// =========================================================================
|
|
|
|
const orchestrationNodes = $derived.by(() => {
|
|
const nodes: Node[] = [];
|
|
if (!collection) return nodes;
|
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
|
if (edge.edgeType !== 'belongs_to') continue;
|
|
const node = nodeStore.get(edge.sourceId);
|
|
if (!node || nodeVisibility(node, userId) === 'hidden') continue;
|
|
if (node.nodeKind === 'orchestration') {
|
|
nodes.push(node);
|
|
}
|
|
}
|
|
nodes.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
return nodes;
|
|
});
|
|
|
|
// =========================================================================
|
|
// State
|
|
// =========================================================================
|
|
|
|
/** Currently selected orchestration node ID */
|
|
let selectedId: string | undefined = $state(undefined);
|
|
|
|
/** Auto-select first orchestration node */
|
|
$effect(() => {
|
|
if (!selectedId && orchestrationNodes.length > 0) {
|
|
selectedId = orchestrationNodes[0].id;
|
|
}
|
|
});
|
|
|
|
const selectedNode = $derived(selectedId ? nodeStore.get(selectedId) : undefined);
|
|
|
|
const selectedMetadata = $derived.by((): Record<string, unknown> => {
|
|
if (!selectedNode?.metadata) return {};
|
|
try {
|
|
return JSON.parse(selectedNode.metadata) as Record<string, unknown>;
|
|
} catch {
|
|
return {};
|
|
}
|
|
});
|
|
|
|
// Editor state
|
|
let scriptContent = $state('');
|
|
let activeTab: 'enkel' | 'teknisk' | 'kompilert' = $state('enkel');
|
|
let compileResult = $state<CompileScriptResponse | null>(null);
|
|
let compiling = $state(false);
|
|
let saving = $state(false);
|
|
let testRunning = $state(false);
|
|
let lastTestJobId: string | null = $state(null);
|
|
|
|
// Trigger state
|
|
let triggerEvent = $state('manual');
|
|
let triggerConditions = $state('');
|
|
let executor = $state('script');
|
|
|
|
// AI-assist
|
|
let showAiAssist = $state(false);
|
|
let aiDescription = $state('');
|
|
let aiGenerating = $state(false);
|
|
let aiError: string | null = $state(null);
|
|
|
|
// History
|
|
let logEntries: OrchestrationLogEntry[] = $state([]);
|
|
let showHistory = $state(false);
|
|
|
|
// Load content when selected node changes
|
|
$effect(() => {
|
|
if (selectedNode) {
|
|
scriptContent = selectedNode.content ?? '';
|
|
const meta = selectedMetadata;
|
|
const trigger = meta.trigger as Record<string, unknown> | undefined;
|
|
triggerEvent = (trigger?.event as string) ?? 'manual';
|
|
const conditions = trigger?.conditions;
|
|
triggerConditions = conditions ? JSON.stringify(conditions, null, 2) : '';
|
|
executor = (meta.executor as string) ?? 'script';
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// Compile (debounced)
|
|
// =========================================================================
|
|
|
|
let compileTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
$effect(() => {
|
|
// React to script changes
|
|
const _content = scriptContent;
|
|
clearTimeout(compileTimeout);
|
|
if (!accessToken || !scriptContent.trim()) {
|
|
compileResult = null;
|
|
return;
|
|
}
|
|
compileTimeout = setTimeout(async () => {
|
|
compiling = true;
|
|
try {
|
|
compileResult = await compileScript(accessToken!, scriptContent);
|
|
} catch (err) {
|
|
console.error('Compile failed:', err);
|
|
} finally {
|
|
compiling = false;
|
|
}
|
|
}, 500);
|
|
});
|
|
|
|
// =========================================================================
|
|
// Derived views
|
|
// =========================================================================
|
|
|
|
const technicalView = $derived(compileResult?.compiled?.technical ?? '(ingen kompilert utdata)');
|
|
|
|
const compiledJson = $derived(
|
|
compileResult?.compiled
|
|
? JSON.stringify(compileResult.compiled, null, 2)
|
|
: '(kompilering feilet eller ingen inndata)'
|
|
);
|
|
|
|
const errorCount = $derived(
|
|
compileResult?.diagnostics?.filter((d) => d.severity === 'Error').length ?? 0
|
|
);
|
|
|
|
const hasErrors = $derived(errorCount > 0);
|
|
|
|
// =========================================================================
|
|
// Actions
|
|
// =========================================================================
|
|
|
|
async function save() {
|
|
if (!accessToken || !selectedId) return;
|
|
saving = true;
|
|
try {
|
|
let conditionsObj: Record<string, unknown> | undefined;
|
|
if (triggerConditions.trim()) {
|
|
try {
|
|
conditionsObj = JSON.parse(triggerConditions);
|
|
} catch {
|
|
// Invalid JSON, skip conditions
|
|
}
|
|
}
|
|
|
|
const metadata: Record<string, unknown> = {
|
|
...selectedMetadata,
|
|
trigger: {
|
|
event: triggerEvent,
|
|
...(conditionsObj ? { conditions: conditionsObj } : {}),
|
|
},
|
|
executor,
|
|
};
|
|
|
|
// Include compiled pipeline if compilation succeeded
|
|
if (compileResult?.compiled) {
|
|
metadata.pipeline = compileResult.compiled.steps;
|
|
if (compileResult.compiled.global_fallback) {
|
|
metadata.global_fallback = compileResult.compiled.global_fallback;
|
|
}
|
|
}
|
|
|
|
await updateNode(accessToken, {
|
|
node_id: selectedId,
|
|
content: scriptContent,
|
|
metadata,
|
|
});
|
|
} catch (err) {
|
|
console.error('Save failed:', err);
|
|
} finally {
|
|
saving = false;
|
|
}
|
|
}
|
|
|
|
async function runTest() {
|
|
if (!accessToken || !selectedId) return;
|
|
// Save first
|
|
await save();
|
|
testRunning = true;
|
|
try {
|
|
const result = await testOrchestration(accessToken, selectedId);
|
|
lastTestJobId = result.job_id;
|
|
} catch (err) {
|
|
console.error('Test run failed:', err);
|
|
} finally {
|
|
testRunning = false;
|
|
}
|
|
}
|
|
|
|
async function loadHistory() {
|
|
if (!accessToken || !selectedId) return;
|
|
try {
|
|
const result = await fetchOrchestrationLog(accessToken, selectedId, 30);
|
|
logEntries = result.entries;
|
|
} catch (err) {
|
|
console.error('Failed to load history:', err);
|
|
}
|
|
}
|
|
|
|
function toggleHistory() {
|
|
showHistory = !showHistory;
|
|
if (showHistory) loadHistory();
|
|
}
|
|
|
|
async function generateWithAi() {
|
|
if (!accessToken || !aiDescription.trim()) return;
|
|
aiGenerating = true;
|
|
aiError = null;
|
|
try {
|
|
const result = await aiSuggestScript(accessToken, {
|
|
description: aiDescription,
|
|
trigger_event: triggerEvent !== 'manual' ? triggerEvent : undefined,
|
|
collection_id: collection?.id,
|
|
});
|
|
|
|
if (result.script) {
|
|
scriptContent = result.script;
|
|
aiDescription = '';
|
|
showAiAssist = false;
|
|
}
|
|
|
|
if (result.status === 'generated_with_errors' && result.compile_result) {
|
|
const errors = result.compile_result.diagnostics
|
|
?.filter((d) => d.severity === 'Error')
|
|
.map((d) => d.message)
|
|
.join('; ');
|
|
if (errors) {
|
|
aiError = `Script generert med feil: ${errors}`;
|
|
}
|
|
} else if (result.status === 'deferred') {
|
|
aiError = null;
|
|
showAiAssist = false;
|
|
}
|
|
} catch (err) {
|
|
aiError = err instanceof Error ? err.message : 'Ukjent feil';
|
|
} finally {
|
|
aiGenerating = false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Trigger event options
|
|
// =========================================================================
|
|
|
|
const TRIGGER_EVENTS = [
|
|
{ value: 'node.created', label: 'Node opprettet' },
|
|
{ value: 'edge.created', label: 'Edge opprettet' },
|
|
{ value: 'communication.ended', label: 'Samtale avsluttet' },
|
|
{ value: 'node.published', label: 'Node publisert' },
|
|
{ value: 'scheduled.due', label: 'Planlagt tidspunkt' },
|
|
{ value: 'manual', label: 'Manuell' },
|
|
];
|
|
|
|
// =========================================================================
|
|
// Helpers
|
|
// =========================================================================
|
|
|
|
function formatTime(iso: string): string {
|
|
try {
|
|
const d = new Date(iso);
|
|
return d.toLocaleString('nb-NO', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="orch-trait">
|
|
{#if orchestrationNodes.length === 0}
|
|
<div class="orch-empty">
|
|
<span class="orch-empty-icon">⚡</span>
|
|
<p>Ingen orkestreringer i denne samlingen.</p>
|
|
<p class="orch-empty-hint">
|
|
Opprett en orchestration-node for å automatisere arbeidsflyter.
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<!-- Orchestration selector (if multiple) -->
|
|
{#if orchestrationNodes.length > 1}
|
|
<div class="orch-selector">
|
|
<select
|
|
bind:value={selectedId}
|
|
class="orch-select"
|
|
>
|
|
{#each orchestrationNodes as node (node.id)}
|
|
<option value={node.id}>{node.title ?? 'Uten tittel'}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
{:else}
|
|
<div class="orch-selector">
|
|
<span class="orch-title">{orchestrationNodes[0]?.title ?? 'Orkestrering'}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Trigger configuration -->
|
|
<div class="orch-trigger">
|
|
<div class="orch-trigger-row">
|
|
<label class="orch-label" for="trigger-event">Trigger</label>
|
|
<select
|
|
id="trigger-event"
|
|
bind:value={triggerEvent}
|
|
class="orch-select orch-select-sm"
|
|
>
|
|
{#each TRIGGER_EVENTS as evt (evt.value)}
|
|
<option value={evt.value}>{evt.label}</option>
|
|
{/each}
|
|
</select>
|
|
|
|
<label class="orch-label" for="executor-select">Executor</label>
|
|
<select
|
|
id="executor-select"
|
|
bind:value={executor}
|
|
class="orch-select orch-select-sm"
|
|
>
|
|
<option value="script">Script</option>
|
|
<option value="bot">Bot</option>
|
|
<option value="dream">Dream</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab bar -->
|
|
<div class="orch-tabs">
|
|
<button
|
|
class="orch-tab"
|
|
class:orch-tab-active={activeTab === 'enkel'}
|
|
onclick={() => { activeTab = 'enkel'; }}
|
|
>
|
|
Enkel
|
|
{#if hasErrors}
|
|
<span class="orch-tab-badge orch-tab-badge-error">{errorCount}</span>
|
|
{/if}
|
|
</button>
|
|
<button
|
|
class="orch-tab"
|
|
class:orch-tab-active={activeTab === 'teknisk'}
|
|
onclick={() => { activeTab = 'teknisk'; }}
|
|
>
|
|
Teknisk
|
|
</button>
|
|
<button
|
|
class="orch-tab"
|
|
class:orch-tab-active={activeTab === 'kompilert'}
|
|
onclick={() => { activeTab = 'kompilert'; }}
|
|
>
|
|
Kompilert
|
|
</button>
|
|
|
|
<div class="orch-tab-spacer"></div>
|
|
|
|
{#if compiling}
|
|
<span class="orch-status orch-status-compiling">Kompilerer...</span>
|
|
{:else if compileResult && !hasErrors}
|
|
<span class="orch-status orch-status-ok">OK</span>
|
|
{:else if hasErrors}
|
|
<span class="orch-status orch-status-error">{errorCount} feil</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Tab content -->
|
|
<div class="orch-editor-area">
|
|
{#if activeTab === 'enkel'}
|
|
<div class="orch-editor-wrapper">
|
|
<textarea
|
|
class="orch-editor"
|
|
bind:value={scriptContent}
|
|
placeholder={"1. transkriber lydfilen (stor modell)\n ved feil: transkriber lydfilen (medium modell)\n2. oppsummer samtalen\n\nved feil: opprett oppgave \"Pipeline feilet\" (bug)"}
|
|
spellcheck="false"
|
|
></textarea>
|
|
|
|
<!-- Inline diagnostics -->
|
|
{#if compileResult?.diagnostics && compileResult.diagnostics.length > 0}
|
|
<div class="orch-diagnostics">
|
|
{#each compileResult.diagnostics as diag (diag.line + '-' + diag.severity)}
|
|
<div
|
|
class="orch-diag"
|
|
class:orch-diag-ok={diag.severity === 'Ok'}
|
|
class:orch-diag-error={diag.severity === 'Error'}
|
|
>
|
|
<span class="orch-diag-icon">
|
|
{diag.severity === 'Ok' ? '\u2713' : '\u2717'}
|
|
</span>
|
|
<span class="orch-diag-line">L{diag.line}</span>
|
|
{#if diag.severity === 'Ok' && diag.compiled_output}
|
|
<span class="orch-diag-text">{diag.compiled_output}</span>
|
|
{:else if diag.severity === 'Error'}
|
|
<span class="orch-diag-text">{diag.message}</span>
|
|
{#if diag.suggestion}
|
|
<span class="orch-diag-suggestion">Mente du: "{diag.suggestion}"?</span>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else if activeTab === 'teknisk'}
|
|
<pre class="orch-readonly">{technicalView}</pre>
|
|
{:else if activeTab === 'kompilert'}
|
|
<pre class="orch-readonly orch-readonly-json">{compiledJson}</pre>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- AI-assist panel -->
|
|
{#if showAiAssist}
|
|
<div class="orch-ai-panel">
|
|
<div class="orch-ai-header">
|
|
<span class="orch-ai-label">AI-assistent</span>
|
|
<button class="orch-btn-close" onclick={() => { showAiAssist = false; }}>×</button>
|
|
</div>
|
|
<textarea
|
|
class="orch-ai-input"
|
|
bind:value={aiDescription}
|
|
placeholder="Beskriv hva orkestreringen skal gjøre..."
|
|
rows="3"
|
|
disabled={aiGenerating}
|
|
></textarea>
|
|
{#if aiError}
|
|
<div class="orch-ai-error">{aiError}</div>
|
|
{/if}
|
|
<div class="orch-ai-actions">
|
|
<button
|
|
class="orch-btn orch-btn-ai"
|
|
onclick={generateWithAi}
|
|
disabled={aiGenerating || !aiDescription.trim()}
|
|
>
|
|
{aiGenerating ? 'Genererer...' : 'Generer script'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Actions -->
|
|
<div class="orch-actions">
|
|
<button
|
|
class="orch-btn orch-btn-primary"
|
|
onclick={save}
|
|
disabled={saving || !accessToken}
|
|
>
|
|
{saving ? 'Lagrer...' : 'Lagre'}
|
|
</button>
|
|
<button
|
|
class="orch-btn orch-btn-test"
|
|
onclick={runTest}
|
|
disabled={testRunning || hasErrors || !accessToken || !scriptContent.trim()}
|
|
title={hasErrors ? 'Rett feilene først' : 'Kjør orkestreringen manuelt'}
|
|
>
|
|
{testRunning ? 'Kjører...' : 'Test kjøring'}
|
|
</button>
|
|
<button
|
|
class="orch-btn orch-btn-ai-toggle"
|
|
onclick={() => { showAiAssist = !showAiAssist; }}
|
|
title="Generer script fra fritekst-beskrivelse med AI"
|
|
>
|
|
{showAiAssist ? 'Skjul AI' : 'AI-assistent'}
|
|
</button>
|
|
<button
|
|
class="orch-btn orch-btn-secondary"
|
|
onclick={toggleHistory}
|
|
>
|
|
{showHistory ? 'Skjul historikk' : 'Historikk'}
|
|
</button>
|
|
|
|
{#if lastTestJobId}
|
|
<span class="orch-test-info">Jobb: {lastTestJobId.slice(0, 8)}...</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- History panel -->
|
|
{#if showHistory}
|
|
<div class="orch-history">
|
|
<div class="orch-history-header">
|
|
<span>Kjørehistorikk</span>
|
|
<button class="orch-btn-refresh" onclick={loadHistory}>Oppdater</button>
|
|
</div>
|
|
{#if logEntries.length === 0}
|
|
<p class="orch-history-empty">Ingen kjøringer ennå.</p>
|
|
{:else}
|
|
<div class="orch-history-list">
|
|
{#each logEntries as entry (entry.id)}
|
|
<div
|
|
class="orch-history-item"
|
|
class:orch-history-ok={entry.status === 'ok'}
|
|
class:orch-history-error={entry.status === 'error'}
|
|
>
|
|
<span class="orch-history-status">
|
|
{entry.status === 'ok' ? '\u2713' : '\u2717'}
|
|
</span>
|
|
<span class="orch-history-step">
|
|
{#if entry.is_fallback}FB{:else}#{entry.step_number}{/if}
|
|
</span>
|
|
<span class="orch-history-binary">{entry.tool_binary}</span>
|
|
{#if entry.duration_ms != null}
|
|
<span class="orch-history-duration">{entry.duration_ms}ms</span>
|
|
{/if}
|
|
<span class="orch-history-time">{formatTime(entry.created_at)}</span>
|
|
{#if entry.error_msg}
|
|
<div class="orch-history-error-msg">{entry.error_msg}</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
/* ================================================================= */
|
|
/* Root */
|
|
/* ================================================================= */
|
|
.orch-trait {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Empty state */
|
|
/* ================================================================= */
|
|
.orch-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
padding: 24px;
|
|
color: #9ca3af;
|
|
text-align: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.orch-empty-icon {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.orch-empty-hint {
|
|
font-size: 11px;
|
|
color: #d1d5db;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Selector */
|
|
/* ================================================================= */
|
|
.orch-selector {
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.orch-title {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.orch-select {
|
|
width: 100%;
|
|
padding: 4px 8px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
background: white;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.orch-select-sm {
|
|
width: auto;
|
|
min-width: 100px;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Trigger config */
|
|
/* ================================================================= */
|
|
.orch-trigger {
|
|
padding: 6px 12px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.orch-trigger-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.orch-label {
|
|
font-size: 11px;
|
|
color: #6b7280;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Tabs */
|
|
/* ================================================================= */
|
|
.orch-tabs {
|
|
display: flex;
|
|
align-items: center;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
flex-shrink: 0;
|
|
padding: 0 8px;
|
|
gap: 0;
|
|
}
|
|
|
|
.orch-tab {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 8px 12px;
|
|
border: none;
|
|
background: transparent;
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
transition: color 0.12s, border-color 0.12s;
|
|
}
|
|
|
|
.orch-tab:hover {
|
|
color: #374151;
|
|
}
|
|
|
|
.orch-tab-active {
|
|
color: #4f46e5;
|
|
border-bottom-color: #4f46e5;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.orch-tab-badge {
|
|
font-size: 10px;
|
|
border-radius: 9999px;
|
|
padding: 0 5px;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.orch-tab-badge-error {
|
|
background: #fef2f2;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.orch-tab-spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
.orch-status {
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 9999px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.orch-status-compiling {
|
|
color: #6b7280;
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.orch-status-ok {
|
|
color: #059669;
|
|
background: #ecfdf5;
|
|
}
|
|
|
|
.orch-status-error {
|
|
color: #dc2626;
|
|
background: #fef2f2;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Editor area */
|
|
/* ================================================================= */
|
|
.orch-editor-area {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.orch-editor-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.orch-editor {
|
|
flex: 1;
|
|
min-height: 120px;
|
|
padding: 10px 12px;
|
|
border: none;
|
|
resize: none;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
color: #1f2937;
|
|
background: #fafbfc;
|
|
outline: none;
|
|
}
|
|
|
|
.orch-editor::placeholder {
|
|
color: #c4c9d0;
|
|
}
|
|
|
|
.orch-readonly {
|
|
flex: 1;
|
|
margin: 0;
|
|
padding: 10px 12px;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
color: #374151;
|
|
background: #f9fafb;
|
|
border: none;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.orch-readonly-json {
|
|
font-size: 11px;
|
|
color: #4b5563;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Diagnostics */
|
|
/* ================================================================= */
|
|
.orch-diagnostics {
|
|
border-top: 1px solid #e5e7eb;
|
|
padding: 6px 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
background: white;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.orch-diag {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 6px;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.orch-diag-icon {
|
|
flex-shrink: 0;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.orch-diag-ok .orch-diag-icon {
|
|
color: #059669;
|
|
}
|
|
|
|
.orch-diag-error .orch-diag-icon {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.orch-diag-line {
|
|
font-size: 10px;
|
|
color: #9ca3af;
|
|
font-variant-numeric: tabular-nums;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.orch-diag-text {
|
|
color: #374151;
|
|
}
|
|
|
|
.orch-diag-error .orch-diag-text {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.orch-diag-suggestion {
|
|
color: #7c3aed;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Actions */
|
|
/* ================================================================= */
|
|
.orch-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
border-top: 1px solid #e5e7eb;
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.orch-btn {
|
|
padding: 5px 12px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 5px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.1s, opacity 0.1s;
|
|
}
|
|
|
|
.orch-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.orch-btn-primary {
|
|
background: #4f46e5;
|
|
color: white;
|
|
border-color: #4f46e5;
|
|
}
|
|
|
|
.orch-btn-primary:hover:not(:disabled) {
|
|
background: #4338ca;
|
|
}
|
|
|
|
.orch-btn-test {
|
|
background: #059669;
|
|
color: white;
|
|
border-color: #059669;
|
|
}
|
|
|
|
.orch-btn-test:hover:not(:disabled) {
|
|
background: #047857;
|
|
}
|
|
|
|
.orch-btn-secondary {
|
|
background: white;
|
|
color: #374151;
|
|
}
|
|
|
|
.orch-btn-secondary:hover:not(:disabled) {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.orch-test-info {
|
|
font-size: 11px;
|
|
color: #6b7280;
|
|
font-family: monospace;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* History panel */
|
|
/* ================================================================= */
|
|
.orch-history {
|
|
border-top: 1px solid #e5e7eb;
|
|
max-height: 240px;
|
|
overflow-y: auto;
|
|
background: #fafbfc;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.orch-history-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 6px 12px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #374151;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
position: sticky;
|
|
top: 0;
|
|
background: #fafbfc;
|
|
}
|
|
|
|
.orch-btn-refresh {
|
|
padding: 2px 8px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 3px;
|
|
font-size: 10px;
|
|
background: white;
|
|
cursor: pointer;
|
|
color: #4b5563;
|
|
}
|
|
|
|
.orch-btn-refresh:hover {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.orch-history-empty {
|
|
padding: 12px;
|
|
text-align: center;
|
|
color: #9ca3af;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.orch-history-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.orch-history-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 5px 12px;
|
|
font-size: 11px;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.orch-history-status {
|
|
flex-shrink: 0;
|
|
font-weight: 700;
|
|
width: 14px;
|
|
text-align: center;
|
|
}
|
|
|
|
.orch-history-ok .orch-history-status {
|
|
color: #059669;
|
|
}
|
|
|
|
.orch-history-error .orch-history-status {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.orch-history-step {
|
|
font-size: 10px;
|
|
color: #6b7280;
|
|
font-variant-numeric: tabular-nums;
|
|
min-width: 20px;
|
|
}
|
|
|
|
.orch-history-binary {
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.orch-history-duration {
|
|
font-size: 10px;
|
|
color: #9ca3af;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.orch-history-time {
|
|
font-size: 10px;
|
|
color: #9ca3af;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.orch-history-error-msg {
|
|
width: 100%;
|
|
padding: 3px 0 3px 22px;
|
|
font-size: 10px;
|
|
color: #dc2626;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* AI-assist panel */
|
|
/* ================================================================= */
|
|
.orch-ai-panel {
|
|
border-top: 1px solid #e5e7eb;
|
|
padding: 8px 12px;
|
|
background: #f0f4ff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.orch-ai-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.orch-ai-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #4f46e5;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.orch-btn-close {
|
|
border: none;
|
|
background: transparent;
|
|
font-size: 16px;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
padding: 0 4px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.orch-btn-close:hover {
|
|
color: #1f2937;
|
|
}
|
|
|
|
.orch-ai-input {
|
|
width: 100%;
|
|
padding: 6px 8px;
|
|
border: 1px solid #c7d2fe;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
resize: vertical;
|
|
min-height: 48px;
|
|
background: white;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.orch-ai-input::placeholder {
|
|
color: #a5b4fc;
|
|
}
|
|
|
|
.orch-ai-input:focus {
|
|
outline: none;
|
|
border-color: #4f46e5;
|
|
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
|
|
}
|
|
|
|
.orch-ai-error {
|
|
font-size: 11px;
|
|
color: #dc2626;
|
|
background: #fef2f2;
|
|
padding: 4px 8px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.orch-ai-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.orch-btn-ai {
|
|
background: #4f46e5;
|
|
color: white;
|
|
border-color: #4f46e5;
|
|
}
|
|
|
|
.orch-btn-ai:hover:not(:disabled) {
|
|
background: #4338ca;
|
|
}
|
|
|
|
.orch-btn-ai-toggle {
|
|
background: #eef2ff;
|
|
color: #4f46e5;
|
|
border-color: #c7d2fe;
|
|
}
|
|
|
|
.orch-btn-ai-toggle:hover:not(:disabled) {
|
|
background: #e0e7ff;
|
|
}
|
|
|
|
/* ================================================================= */
|
|
/* Responsive */
|
|
/* ================================================================= */
|
|
@container (max-width: 400px) {
|
|
.orch-trigger-row {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 4px;
|
|
}
|
|
|
|
.orch-select-sm {
|
|
width: 100%;
|
|
}
|
|
|
|
.orch-actions {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.orch-btn {
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.orch-test-info {
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.orch-trigger-row {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 4px;
|
|
}
|
|
|
|
.orch-select-sm {
|
|
width: 100%;
|
|
}
|
|
|
|
.orch-editor {
|
|
min-height: 100px;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
</style>
|