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>
118 lines
2.9 KiB
Vue
118 lines
2.9 KiB
Vue
<template>
|
|
<div
|
|
class="flex mb-3"
|
|
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
|
|
>
|
|
<div
|
|
class="max-w-[90%] rounded-lg px-3 py-2 text-sm bg-[#161b22] border-l-2"
|
|
:class="message.role === 'user' ? 'border-[#58a6ff]' : 'border-[#3fb950]'"
|
|
>
|
|
<!-- Rendered content -->
|
|
<div
|
|
v-if="message.role === 'assistant'"
|
|
class="copilot-md text-[#e0e0e0] leading-relaxed break-words"
|
|
v-html="renderedContent"
|
|
/>
|
|
<div v-else class="text-[#e0e0e0] leading-relaxed break-words whitespace-pre-wrap">
|
|
{{ message.content }}
|
|
</div>
|
|
|
|
<!-- Tool call visualizations (assistant only) -->
|
|
<CopilotToolViz
|
|
v-for="tc in message.toolCalls"
|
|
:key="tc.id"
|
|
:tool-call="tc"
|
|
/>
|
|
|
|
<!-- Streaming cursor -->
|
|
<span
|
|
v-if="isStreaming && message.role === 'assistant'"
|
|
class="inline-block w-2 h-4 bg-[#3fb950] align-middle animate-pulse ml-0.5"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from "vue";
|
|
import type { Message } from "@/stores/copilot.store";
|
|
import CopilotToolViz from "./CopilotToolViz.vue";
|
|
|
|
const props = defineProps<{
|
|
message: Message;
|
|
isStreaming?: boolean;
|
|
}>();
|
|
|
|
/**
|
|
* Simple regex-based markdown renderer.
|
|
* Handles: bold, inline code, code blocks, list items, newlines.
|
|
* No external library needed.
|
|
*/
|
|
const renderedContent = computed(() => {
|
|
let text = props.message.content;
|
|
|
|
// Escape HTML
|
|
text = text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
// Code blocks (```...```)
|
|
text = text.replace(
|
|
/```(\w*)\n?([\s\S]*?)```/g,
|
|
'<pre class="bg-[#0d1117] rounded p-2 my-1.5 overflow-x-auto text-xs font-mono"><code>$2</code></pre>',
|
|
);
|
|
|
|
// Inline code (`...`)
|
|
text = text.replace(
|
|
/`([^`]+)`/g,
|
|
'<code class="bg-[#0d1117] text-[#79c0ff] px-1 py-0.5 rounded text-xs font-mono">$1</code>',
|
|
);
|
|
|
|
// Bold (**...**)
|
|
text = text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
|
|
// Italic (*...*)
|
|
text = text.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
|
|
|
// List items (- item)
|
|
text = text.replace(/^- (.+)$/gm, '<li class="ml-3 list-disc">$1</li>');
|
|
|
|
// Wrap consecutive <li> in <ul>
|
|
text = text.replace(
|
|
/(<li[^>]*>.*?<\/li>\n?)+/g,
|
|
'<ul class="my-1">$&</ul>',
|
|
);
|
|
|
|
// Newlines to <br> (except inside <pre>)
|
|
text = text.replace(/\n(?!<\/?pre|<\/?ul|<\/?li)/g, "<br>");
|
|
|
|
return text;
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Markdown styling within copilot messages */
|
|
.copilot-md :deep(strong) {
|
|
color: #e0e0e0;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.copilot-md :deep(em) {
|
|
color: #8b949e;
|
|
font-style: italic;
|
|
}
|
|
|
|
.copilot-md :deep(code) {
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
}
|
|
|
|
.copilot-md :deep(pre) {
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
}
|
|
|
|
.copilot-md :deep(ul) {
|
|
padding-left: 0.5rem;
|
|
}
|
|
</style>
|