wraith/frontend/src/components/copilot/CopilotMessage.vue
Vantz Stockwell be868e8172 feat: AI copilot panel — chat UI with streaming, tool visualization, OAuth settings
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>
2026-03-17 09:03:33 -04:00

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
// 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>