This commit is contained in:
2026-04-21 13:19:17 +03:00
commit 1d52096e38
21 changed files with 8991 additions and 0 deletions

2252
web/app.js Normal file

File diff suppressed because it is too large Load Diff

406
web/index.html Normal file
View File

@@ -0,0 +1,406 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GoCoder</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="style.css">
<style>
.conversation-item {
border-left: 3px solid transparent;
}
.conversation-item:hover {
border-left-color: #cbd5e1;
}
.conversation-item.active {
border-left-color: #3b82f6;
background-color: #eff6ff;
color: #1d4ed8;
}
.conversation-item.active .text-gray-400 {
color: #3b82f6;
}
.loading-dots {
display: inline-flex;
align-items: center;
}
.loading-dots .dot {
animation: blink 1.4s infinite both;
animation-duration: 1.4s;
}
.loading-dots .dot:nth-child(2) {
animation-delay: 0.2s;
}
.loading-dots .dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0%, 80%, 100% { opacity: 0; }
40% { opacity: 1; }
}
.sidebar {
transition: transform 0.2s ease;
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
z-index: 50;
height: 100vh;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.3);
z-index: 40;
}
.sidebar-overlay.open {
display: block;
}
}
</style>
</head>
<body class="min-h-screen bg-gray-50">
<div class="flex h-screen">
<!-- Sidebar -->
<div class="sidebar w-72 bg-white border-r border-gray-200 flex flex-col" id="sidebar">
<div class="p-4 border-b border-gray-200">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-gray-800">Workspaces</h2>
<div class="flex gap-1">
<button id="newGroupBtn" class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-600 transition-colors" title="New workspace">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</button>
<button id="newConversationBtn" class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-600 transition-colors" title="New conversation">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
</div>
</div>
</div>
<div id="conversationList" class="flex-1 overflow-y-auto p-2 space-y-3">
</div>
<div class="p-3 border-t border-gray-200">
<button id="mobileMenuBtn" class="md:hidden w-full text-center text-sm text-gray-500 hover:text-gray-700 py-2">
Close sidebar
</button>
</div>
</div>
<!-- Sidebar overlay for mobile -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- Group settings modal -->
<div id="groupSettingsModal" class="fixed inset-0 z-50 hidden items-center justify-center" style="background: rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 overflow-hidden">
<div class="p-6">
<div class="flex items-center justify-between mb-5">
<h3 class="text-lg font-semibold text-gray-800">Workspace Settings</h3>
<button id="closeGroupSettingsModal" class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<form id="groupSettingsForm">
<label class="block text-sm font-medium text-gray-700 mb-1.5">Workspace name</label>
<input
type="text"
id="groupSettingsNameInput"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800"
required
>
<label class="block text-sm font-medium text-gray-700 mb-1.5 mt-4">Workspace folder</label>
<div id="groupSettingsLocation" class="hidden mb-3 px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-400 flex-shrink-0"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
<span id="groupSettingsLocationText" class="text-sm text-gray-600 truncate"></span>
</div>
</div>
<button type="button" id="groupSettingsPickBtn" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-gray-700 font-medium hover:bg-gray-50 transition-colors flex items-center justify-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
Pick Folder
</button>
<div class="flex gap-3 mt-5">
<button type="button" id="cancelGroupSettingsBtn" class="flex-1 px-4 py-2.5 border border-gray-300 rounded-xl text-gray-700 font-medium hover:bg-gray-50 transition-colors">
Cancel
</button>
<button type="submit" class="flex-1 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors">
Save
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Runtime settings modal -->
<div id="settingsModal" class="fixed inset-0 z-50 hidden items-center justify-center p-4" style="background: rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
<div class="max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-800">Runtime Settings</h3>
<p class="mt-1 text-sm text-gray-500">Edit the model endpoint, generation params, server settings, and compaction budget used by the chat runtime.</p>
</div>
<button id="closeSettingsModal" class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<form id="settingsForm">
<div class="space-y-4">
<div class="rounded-2xl border border-gray-200 bg-gray-50/70 p-4">
<div class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
<span>Connection</span>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsProvider">Provider</label>
<input type="text" id="settingsProvider" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsModel">Model</label>
<input type="text" id="settingsModel" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsBaseURL">Base URL</label>
<input type="url" id="settingsBaseURL" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsAPIKey">API key</label>
<input type="password" id="settingsAPIKey" autocomplete="off" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
<p class="mt-1 text-xs text-gray-500">Leave as-is to keep the current key or replace it with a new one.</p>
</div>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-gray-50/70 p-4">
<div class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
<span>Server</span>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsServerHost">Host</label>
<input type="text" id="settingsServerHost" required placeholder="localhost" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsServerAddr">Address</label>
<input type="text" id="settingsServerAddr" required placeholder=":8080" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div class="md:col-span-2">
<p class="text-xs text-gray-500">Server settings are saved immediately but only take effect after you restart the app.</p>
</div>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-gray-50/70 p-4">
<div class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
<span>Generation</span>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsTemperature">Temperature</label>
<input type="number" id="settingsTemperature" min="0" step="0.01" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsMaxTokens">Max tokens</label>
<input type="number" id="settingsMaxTokens" min="1" step="1" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsTopP">Top P</label>
<input type="number" id="settingsTopP" min="0.01" max="1" step="0.01" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsFrequencyPenalty">Frequency penalty</label>
<input type="number" id="settingsFrequencyPenalty" step="0.1" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsPresencePenalty">Presence penalty</label>
<input type="number" id="settingsPresencePenalty" step="0.1" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsReasoningEffort">Reasoning effort</label>
<select id="settingsReasoningEffort" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
<option value="">Default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<p class="mt-1 text-xs text-gray-500">Forwarded to LM Studio when the backend supports reasoning controls.</p>
</div>
<div class="md:col-span-2 flex items-start gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3">
<input type="checkbox" id="settingsAutoOpenReasoning" class="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<div class="min-w-0">
<label class="block text-sm font-medium text-gray-700" for="settingsAutoOpenReasoning">Auto-open reasoning</label>
<p class="mt-1 text-xs text-gray-500">When enabled, live reasoning starts expanded. You can still collapse it while the model is thinking.</p>
</div>
</div>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-gray-50/70 p-4">
<div class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
<span>Context compaction</span>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsContextTokens">Prompt budget (tokens)</label>
<input type="number" id="settingsContextTokens" min="1" step="1" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
<p class="mt-1 text-xs text-gray-500">This is the estimated prompt size budget used before compaction starts.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsTriggerRatio">Compaction trigger</label>
<input type="number" id="settingsTriggerRatio" min="0.01" max="0.99" step="0.01" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
<p class="mt-1 text-xs text-gray-500">Compaction starts above this fraction of the budget.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5" for="settingsTargetRatio">Compaction target</label>
<input type="number" id="settingsTargetRatio" min="0.01" max="0.99" step="0.01" required class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800">
<p class="mt-1 text-xs text-gray-500">The summarizer tries to shrink history below this fraction.</p>
</div>
</div>
</div>
</div>
<p id="settingsFormError" class="mt-4 hidden text-sm text-red-600"></p>
<div class="flex gap-3 mt-5">
<button type="button" id="cancelSettingsBtn" class="flex-1 px-4 py-2.5 border border-gray-300 rounded-xl text-gray-700 font-medium hover:bg-gray-50 transition-colors">
Cancel
</button>
<button type="submit" class="flex-1 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors">
Save
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Group creation modal -->
<div id="groupModal" class="fixed inset-0 z-50 hidden items-center justify-center" style="background: rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 overflow-hidden">
<div class="p-6">
<div class="flex items-center justify-between mb-5">
<h3 class="text-lg font-semibold text-gray-800">New Workspace</h3>
<button id="closeGroupModal" class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<form id="groupForm">
<label class="block text-sm font-medium text-gray-700 mb-1.5">Workspace name</label>
<input
type="text"
id="groupNameInput"
placeholder="e.g. My Project"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 placeholder:text-gray-400"
required
autofocus
>
<div class="flex gap-3 mt-5">
<button type="button" id="cancelGroupBtn" class="flex-1 px-4 py-2.5 border border-gray-300 rounded-xl text-gray-700 font-medium hover:bg-gray-50 transition-colors">
Cancel
</button>
<button type="submit" class="flex-1 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors">
Create
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Main content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Header -->
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center gap-4">
<button id="mobileMenuBtnDesktop" class="md:hidden p-2 rounded-lg hover:bg-gray-100 text-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="flex items-center gap-2 flex-1 min-w-0">
<h1 class="text-xl font-bold text-gray-800">GoCoder</h1>
<span class="text-gray-300 text-lg leading-none mx-1 flex-shrink-0"></span>
<div id="headerLocation" class="hidden items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-400"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
<span id="headerLocationText" class="text-sm text-gray-500 truncate max-w-xs"></span>
<button id="changeLocationBtn" class="hidden p-1 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors" title="Change location">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</button>
</div>
<button id="pickLocationBtn" class="hidden items-center gap-1 px-2.5 py-1 text-xs bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg transition-colors border border-gray-200">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
Pick Location
</button>
</div>
<button id="openSettingsBtn" class="p-2 rounded-lg hover:bg-gray-100 text-gray-500 hover:text-gray-700 transition-colors" title="Runtime settings">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
<!-- Chat area -->
<div id="chatScrollArea" class="flex-1 overflow-y-auto px-4 md:px-8 lg:px-16 py-6">
<div id="result" class="hidden">
<div id="resultText" class="space-y-4"></div>
</div>
<div id="emptyState" class="flex flex-col items-center justify-center h-full text-center">
<div class="w-20 h-20 bg-gray-100 rounded-2xl flex items-center justify-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h2 class="text-2xl font-semibold text-gray-700 mb-2">Start a conversation</h2>
<p class="text-gray-500 mb-6">Type a message to begin, or select an existing conversation</p>
<button id="newConversationBtnDesktop" class="bg-blue-600 hover:bg-blue-700 text-white font-medium px-6 py-3 rounded-lg transition-colors">
New Conversation
</button>
</div>
</div>
<!-- Input area -->
<div class="bg-white border-t border-gray-200 px-4 md:px-8 lg:px-16 py-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs text-gray-500 font-medium">Agent:</span>
<div id="agentSelector" class="flex gap-1.5 flex-wrap">
<button type="button" data-agent="none" class="agent-btn px-3 py-1 text-xs font-medium rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition-colors bg-blue-50 border-blue-300 text-blue-700">None</button>
<button type="button" data-agent="dadjokes" class="agent-btn px-3 py-1 text-xs font-medium rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition-colors">Dadjokes</button>
<button type="button" data-agent="poet" class="agent-btn px-3 py-1 text-xs font-medium rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition-colors">Poet</button>
<button type="button" data-agent="robot" class="agent-btn px-3 py-1 text-xs font-medium rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition-colors">Robot</button>
</div>
</div>
<form id="messageForm" class="flex items-end gap-3">
<textarea
id="messageInput"
name="message"
rows="1"
placeholder="Type a message... Enter sends, Shift+Enter adds a new line"
aria-describedby="messageInputHint"
class="message-input flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800"
required
></textarea>
<button
type="submit"
id="sendBtn"
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-medium px-6 py-3 rounded-xl transition-colors"
>
Send
</button>
<button
type="button"
id="cancelBtn"
class="hidden border border-red-200 bg-white px-5 py-3 font-medium text-red-600 rounded-xl transition-colors hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
>
Stop
</button>
</form>
<div id="messageInputHint" class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-gray-500">
<span><kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Enter</kbd> sends</span>
<span><kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Shift</kbd>+<kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Enter</kbd> adds a new line</span>
<span><kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Alt</kbd>, <kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Ctrl</kbd>, or <kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Cmd</kbd>+<kbd class="rounded border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-700">Enter</kbd> also add a new line</span>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

302
web/style.css Normal file
View File

@@ -0,0 +1,302 @@
.markdown-body {
line-height: 1.6;
}
.markdown-body p {
margin: 0.5em 0;
}
.markdown-body p:first-child {
margin-top: 0;
}
.markdown-body p:last-child {
margin-bottom: 0;
}
.markdown-body pre {
background: #1e1e2e;
color: #cdd6f4;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 0.5em 0;
}
.markdown-body code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.875em;
}
.markdown-body :not(pre) > code {
background: #e5e7eb;
color: #1e40af;
padding: 0.125em 0.375em;
border-radius: 0.25rem;
}
.markdown-body pre code {
background: none;
padding: 0;
color: inherit;
font-size: 0.8125rem;
line-height: 1.7;
}
.markdown-body blockquote {
border-left: 3px solid #d1d5db;
padding-left: 1rem;
color: #6b7280;
margin: 0.5em 0;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 1.5rem;
margin: 0.5em 0;
}
.markdown-body li {
margin: 0.25em 0;
}
.markdown-body li > p {
display: inline;
}
.markdown-body hr {
border: none;
border-top: 1px solid #d1d5db;
margin: 1em 0;
}
.markdown-body table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
margin: 0.5em 0;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
}
.markdown-body th,
.markdown-body td {
border: none;
padding: 0.5rem 0.875rem;
text-align: left;
}
.markdown-body th {
background: #f9fafb;
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
}
.markdown-body tr:nth-child(even) {
background: #fafbfc;
}
.markdown-body tr:nth-child(odd) {
background: #ffffff;
}
.markdown-body tr:hover {
background: #f3f4f6;
}
.markdown-body a {
color: #2563eb;
text-decoration: underline;
}
.markdown-body img {
max-width: 100%;
border-radius: 0.375rem;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin: 1em 0 0.5em 0;
font-weight: 600;
line-height: 1.3;
}
.markdown-body h1 {
font-size: 1.5rem;
}
.markdown-body h2 {
font-size: 1.25rem;
}
.markdown-body h3 {
font-size: 1.125rem;
}
.thinking-panel > summary {
list-style: none;
}
.thinking-panel > summary::-webkit-details-marker {
display: none;
}
.thinking-chevron {
transition: transform 0.15s ease;
}
.thinking-panel[open] .thinking-chevron {
transform: rotate(90deg);
}
.tool-meta-list {
display: grid;
gap: 0.5rem;
}
.tool-meta-row {
display: grid;
grid-template-columns: 6.5rem minmax(0, 1fr);
gap: 0.75rem;
align-items: start;
}
.tool-meta-key {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #94a3b8;
}
.tool-meta-value {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.8125rem;
line-height: 1.5;
color: #0f172a;
white-space: pre-wrap;
word-break: break-word;
}
.tool-text-block {
margin: 0;
max-height: 18rem;
overflow: auto;
border: 1px solid #dbe4f0;
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.95);
padding: 0.875rem 1rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.8125rem;
line-height: 1.5rem;
color: #0f172a;
white-space: pre-wrap;
word-break: break-word;
}
.tool-text-block-error {
border-color: #fecaca;
background: #fff1f2;
color: #7f1d1d;
}
.tool-placeholder {
border: 1px dashed #cbd5e1;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #64748b;
font-style: italic;
}
.tool-file-preview {
max-height: 12.25rem;
overflow-y: auto;
border: 1px solid #dbe4f0;
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.95);
}
.tool-file-preview.is-expanded {
max-height: 60vh;
}
.tool-file-preview pre {
margin: 0;
padding: 0.875rem 1rem;
background: transparent;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.8125rem;
line-height: 1.5rem;
color: inherit;
white-space: pre-wrap;
word-break: break-word;
}
.tool-diff {
overflow: hidden;
border-radius: 0.75rem;
border: 1px solid #0f172a;
background: #0f172a;
}
.tool-diff-header {
padding: 0.625rem 0.875rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
color: #cbd5e1;
}
.tool-diff-body {
max-height: 18rem;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem;
line-height: 1.5rem;
}
.tool-diff-line {
display: flex;
gap: 0.5rem;
padding: 0 0.875rem;
white-space: pre-wrap;
word-break: break-word;
}
.tool-diff-prefix {
flex: 0 0 1rem;
text-align: center;
user-select: none;
opacity: 0.7;
}
.tool-diff-remove {
background: rgba(239, 68, 68, 0.08);
color: #fecaca;
}
.tool-diff-add {
background: rgba(16, 185, 129, 0.08);
color: #a7f3d0;
}
.tool-diff-note {
padding: 0.875rem;
color: #cbd5e1;
}
.message-input {
resize: none;
overflow-y: hidden;
min-height: 3.5rem;
line-height: 1.5rem;
}