Add the XO copilot right-side panel with: - Pinia store (copilot.store.ts) managing panel state, messages, streaming, and token tracking - useCopilot composable with mock streaming (30-100ms/word) and context-aware responses - CopilotPanel: 320px collapsible panel with header, scrollable message list, and input area - CopilotMessage: markdown rendering (bold, code, lists) with streaming cursor animation - CopilotToolViz: color-coded tool call cards (green=terminal, yellow=SFTP, blue=RDP) with pending spinner, done checkmark, expandable input/result JSON - CopilotSettings: model selector, token usage, clear history, connect/disconnect - MainLayout integration: ghost icon toggle in toolbar, Ctrl+Shift+K shortcut, CSS slide transition, content area resizes (not overlay) All Wails AIService bindings are TODOs with mock behavior. Pure Tailwind CSS, no external markdown library. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
4.8 KiB
Vue
138 lines
4.8 KiB
Vue
<template>
|
|
<!-- Backdrop -->
|
|
<div
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
@click.self="emit('close')"
|
|
>
|
|
<!-- Modal -->
|
|
<div class="w-80 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
|
|
<h3 class="text-sm font-semibold text-[#e0e0e0]">XO Settings</h3>
|
|
<button
|
|
class="text-[#8b949e] hover:text-[#e0e0e0] transition-colors cursor-pointer"
|
|
@click="emit('close')"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="px-4 py-4 space-y-4">
|
|
<!-- Auth section -->
|
|
<div v-if="!store.isAuthenticated">
|
|
<button
|
|
class="w-full px-3 py-2 text-sm font-medium text-white bg-[#1f6feb] hover:bg-[#388bfd] rounded transition-colors cursor-pointer"
|
|
@click="handleLogin"
|
|
>
|
|
Connect to Claude
|
|
</button>
|
|
|
|
<div class="mt-3">
|
|
<label class="block text-xs text-[#8b949e] mb-1">Or enter API Key</label>
|
|
<input
|
|
v-model="apiKey"
|
|
type="password"
|
|
placeholder="sk-ant-..."
|
|
class="w-full px-2.5 py-1.5 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[#e0e0e0] placeholder-[#484f58] outline-none focus:border-[#58a6ff] transition-colors"
|
|
/>
|
|
<button
|
|
class="mt-2 w-full px-3 py-1.5 text-xs font-medium text-[#e0e0e0] bg-[#21262d] hover:bg-[#30363d] border border-[#30363d] rounded transition-colors cursor-pointer"
|
|
:disabled="!apiKey.trim()"
|
|
@click="handleApiKey"
|
|
>
|
|
Authenticate
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model selector -->
|
|
<div>
|
|
<label class="block text-xs text-[#8b949e] mb-1">Model</label>
|
|
<select
|
|
v-model="store.model"
|
|
class="w-full px-2.5 py-1.5 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] transition-colors cursor-pointer appearance-none"
|
|
>
|
|
<option value="claude-sonnet-4-5-20250514">claude-sonnet-4-5-20250514</option>
|
|
<option value="claude-opus-4-5-20250414">claude-opus-4-5-20250414</option>
|
|
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Token usage -->
|
|
<div>
|
|
<label class="block text-xs text-[#8b949e] mb-1">Token Usage</label>
|
|
<div class="flex items-center gap-3 text-xs text-[#e0e0e0]">
|
|
<span>
|
|
<span class="text-[#8b949e]">In:</span> {{ formatTokens(store.tokenUsage.input) }}
|
|
</span>
|
|
<span>
|
|
<span class="text-[#8b949e]">Out:</span> {{ formatTokens(store.tokenUsage.output) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="space-y-2 pt-2 border-t border-[#30363d]">
|
|
<button
|
|
class="w-full px-3 py-1.5 text-xs font-medium text-[#e0e0e0] bg-[#21262d] hover:bg-[#30363d] border border-[#30363d] rounded transition-colors cursor-pointer"
|
|
@click="handleClearHistory"
|
|
>
|
|
Clear History
|
|
</button>
|
|
<button
|
|
v-if="store.isAuthenticated"
|
|
class="w-full px-3 py-1.5 text-xs font-medium text-[#f85149] bg-[#21262d] hover:bg-[#30363d] border border-[#30363d] rounded transition-colors cursor-pointer"
|
|
@click="handleDisconnect"
|
|
>
|
|
Disconnect
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from "vue";
|
|
import { useCopilotStore } from "@/stores/copilot.store";
|
|
|
|
const store = useCopilotStore();
|
|
const apiKey = ref("");
|
|
|
|
const emit = defineEmits<{
|
|
(e: "close"): void;
|
|
}>();
|
|
|
|
function handleLogin(): void {
|
|
// TODO: Wails AIService.StartLogin()
|
|
store.startLogin();
|
|
emit("close");
|
|
}
|
|
|
|
function handleApiKey(): void {
|
|
if (!apiKey.value.trim()) return;
|
|
// TODO: Wails AIService.SetApiKey(apiKey)
|
|
store.isAuthenticated = true;
|
|
apiKey.value = "";
|
|
emit("close");
|
|
}
|
|
|
|
function handleClearHistory(): void {
|
|
store.clearHistory();
|
|
emit("close");
|
|
}
|
|
|
|
function handleDisconnect(): void {
|
|
store.disconnect();
|
|
emit("close");
|
|
}
|
|
|
|
function formatTokens(n: number): string {
|
|
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
return String(n);
|
|
}
|
|
</script>
|