-
Notifications
You must be signed in to change notification settings - Fork 2
feat(web): add Vue 3 + shadcn-style chat dashboard UI #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5c53f89
c65e1ef
ca4224c
27088bf
87cb373
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,23 @@ | ||
| # Dashboard | ||
|
|
||
| Corvus web dashboard - coming soon. | ||
| Corvus web dashboard implemented with Vue 3 + Vite and shadcn-vue style components. | ||
|
|
||
| ## Development Plan | ||
| ## Features | ||
|
|
||
| - **Framework**: Vue/React (TBD) | ||
| - **Features**: | ||
| - Authentication | ||
| - Agent management | ||
| - Analytics dashboard | ||
| - Settings panel | ||
| - Real-time updates | ||
| - Basic chat workspace aligned with the Corvus system design: | ||
| - Header with model name | ||
| - Chat panel with user/assistant bubbles | ||
| - Gateway config panel (base URL, pairing code, bearer token, webhook secret) | ||
| - Message composer with send action | ||
| - Local state only (mock assistant responses for now) | ||
| - Tailwind CSS v4 styling with reusable shadcn-vue-inspired UI primitives (`Button`, `Input`) | ||
|
|
||
| ## Getting Started | ||
| ## Run | ||
|
|
||
| ```bash | ||
| # From web root | ||
| # From clients/web | ||
| pnpm install | ||
| pnpm dev:dashboard | ||
| ``` | ||
|
|
||
| Dashboard runs on <http://localhost:4323>. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "$schema": "https://www.shadcn-vue.com/schema.json", | ||
| "style": "default", | ||
| "typescript": true, | ||
| "tailwind": { | ||
| "config": "", | ||
| "css": "src/style.css", | ||
| "baseColor": "slate", | ||
| "cssVariables": true | ||
| }, | ||
| "aliases": { | ||
| "components": "@/components", | ||
| "utils": "@/lib/utils" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Corvus Dashboard</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.ts"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,13 +5,30 @@ | |||||||||||||||||||||||||||||||
| "description": "Corvus Web Dashboard", | ||||||||||||||||||||||||||||||||
| "type": "module", | ||||||||||||||||||||||||||||||||
| "scripts": { | ||||||||||||||||||||||||||||||||
| "dev": "echo 'Dashboard app not yet implemented' && exit 1", | ||||||||||||||||||||||||||||||||
| "build": "echo 'Dashboard app not yet implemented' && exit 1", | ||||||||||||||||||||||||||||||||
| "preview": "echo 'Dashboard app not yet implemented' && exit 1", | ||||||||||||||||||||||||||||||||
| "format": "echo 'No files to format'", | ||||||||||||||||||||||||||||||||
| "check": "echo 'No files to check'", | ||||||||||||||||||||||||||||||||
| "clean": "rm -rf dist" | ||||||||||||||||||||||||||||||||
| "dev": "vite --port 4323", | ||||||||||||||||||||||||||||||||
| "build": "vue-tsc -b && vite build", | ||||||||||||||||||||||||||||||||
| "preview": "vite preview --port 4323", | ||||||||||||||||||||||||||||||||
| "format": "biome format --write src package.json components.json tsconfig*.json vite.config.ts index.html postcss.config.js", | ||||||||||||||||||||||||||||||||
| "check": "biome check src package.json components.json tsconfig*.json vite.config.ts index.html postcss.config.js" | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| "dependencies": {}, | ||||||||||||||||||||||||||||||||
| "devDependencies": {} | ||||||||||||||||||||||||||||||||
| "dependencies": { | ||||||||||||||||||||||||||||||||
| "class-variance-authority": "^0.7.1", | ||||||||||||||||||||||||||||||||
| "clsx": "^2.1.1", | ||||||||||||||||||||||||||||||||
| "tailwind-merge": "^3.3.1", | ||||||||||||||||||||||||||||||||
| "vue": "^3.5.22" | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| "devDependencies": { | ||||||||||||||||||||||||||||||||
| "@biomejs/biome": "2.3.15", | ||||||||||||||||||||||||||||||||
| "@tailwindcss/postcss": "^4.1.16", | ||||||||||||||||||||||||||||||||
| "@types/node": "^24.10.1", | ||||||||||||||||||||||||||||||||
| "@vitejs/plugin-vue": "^6.0.1", | ||||||||||||||||||||||||||||||||
| "@vue/tsconfig": "^0.8.1", | ||||||||||||||||||||||||||||||||
| "autoprefixer": "^10.4.21", | ||||||||||||||||||||||||||||||||
| "postcss": "^8.5.6", | ||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Remove Tailwind v4 has built-in Lightning CSS integration that handles vendor prefixes automatically. The ♻️ Proposed fix "devDependencies": {
"@biomejs/biome": "2.3.15",
"@tailwindcss/postcss": "^4.1.16",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
- "autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.16",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| "tailwindcss": "^4.1.16", | ||||||||||||||||||||||||||||||||
| "typescript": "^5.9.3", | ||||||||||||||||||||||||||||||||
| "vite": "^7.1.10", | ||||||||||||||||||||||||||||||||
| "vue-tsc": "^3.1.1", | ||||||||||||||||||||||||||||||||
| "@tsconfig/node22": "^22.0.2" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export default { | ||
| plugins: { | ||
| "@tailwindcss/postcss": {}, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| <script setup lang="ts"> | ||
| import { computed, ref } from "vue"; | ||
|
|
||
| type Role = "assistant" | "user"; | ||
|
|
||
| interface Message { | ||
| id: number; | ||
| role: Role; | ||
| content: string; | ||
| } | ||
|
|
||
| const modelName = "Corvus Agent"; | ||
| const _showConfig = ref(false); | ||
| const prompt = ref(""); | ||
| const baseUrl = ref("http://127.0.0.1:3000"); | ||
| const _pairingCode = ref(""); | ||
| const _bearerToken = ref(""); | ||
| const _webhookSecret = ref(""); | ||
|
|
||
| const messages = ref<Message[]>([ | ||
| { | ||
| id: 0, | ||
| role: "assistant", | ||
| content: `Hola, soy ${modelName}. ¿En qué puedo ayudarte?`, | ||
| }, | ||
| ]); | ||
|
|
||
| const _canSend = computed(() => prompt.value.trim().length > 0); | ||
|
|
||
| function _sendMessage() { | ||
| const text = prompt.value.trim(); | ||
| if (!text) { | ||
| return; | ||
| } | ||
|
|
||
| messages.value.push({ id: Date.now(), role: "user", content: text }); | ||
| messages.value.push({ | ||
| id: Date.now() + 1, | ||
| role: "assistant", | ||
| content: `Procesando "${text}" con ${modelName}. Gateway: ${baseUrl.value}`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User-supplied text interpolated directly into the mock response.
🤖 Prompt for AI Agents |
||
| }); | ||
| prompt.value = ""; | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <main class="min-h-screen bg-slate-50 text-slate-900"> | ||
| <section class="mx-auto flex w-full max-w-4xl flex-col gap-4 p-4 md:p-6"> | ||
| <header class="flex items-center justify-between rounded-xl border border-slate-200 bg-white p-4"> | ||
| <div> | ||
| <h1 class="text-xl font-semibold">{{ modelName }}</h1> | ||
| <p class="text-sm text-slate-500"> | ||
| {{ showConfig ? 'Configuración del gateway' : 'Simple AI chat' }} | ||
| </p> | ||
| </div> | ||
| <Button variant="ghost" size="sm" @click="showConfig = !showConfig"> | ||
| {{ showConfig ? 'Volver al chat' : 'Config' }} | ||
| </Button> | ||
| </header> | ||
|
|
||
| <section | ||
| v-if="showConfig" | ||
| class="grid gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm" | ||
| > | ||
| <label class="space-y-1 text-sm"> | ||
| <span class="font-medium">Base URL</span> | ||
| <Input v-model="baseUrl" placeholder="http://127.0.0.1:3000" /> | ||
| </label> | ||
| <label class="space-y-1 text-sm"> | ||
| <span class="font-medium">Pairing code</span> | ||
| <Input v-model="pairingCode" placeholder="Pairing code" /> | ||
| </label> | ||
| <label class="space-y-1 text-sm"> | ||
| <span class="font-medium">Bearer token</span> | ||
| <Input v-model="bearerToken" placeholder="Bearer token" type="password" /> | ||
| </label> | ||
| <label class="space-y-1 text-sm"> | ||
| <span class="font-medium">Webhook secret</span> | ||
| <Input v-model="webhookSecret" placeholder="Webhook secret" type="password" /> | ||
| </label> | ||
| </section> | ||
|
|
||
| <section v-else class="flex min-h-[70vh] flex-col rounded-xl border border-slate-200 bg-white shadow-sm"> | ||
| <div class="flex-1 space-y-3 overflow-y-auto p-4"> | ||
| <ChatMessage | ||
| v-for="message in messages" | ||
| :key="message.id" | ||
| :role="message.role" | ||
| :content="message.content" | ||
| /> | ||
| </div> | ||
|
|
||
| <form class="flex gap-2 border-t border-slate-200 p-3" @submit.prevent="sendMessage"> | ||
| <Input v-model="prompt" placeholder="Escribe un mensaje..." /> | ||
| <Button type="submit" :disabled="!canSend">Enviar</Button> | ||
| </form> | ||
| </section> | ||
| </section> | ||
| </main> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <script setup lang="ts"> | ||
| defineProps<{ | ||
| role: "assistant" | "user"; | ||
| content: string; | ||
| }>(); | ||
| </script> | ||
|
|
||
| <template> | ||
| <div :class="['flex w-full', role === 'user' ? 'justify-end' : 'justify-start']"> | ||
| <div | ||
| :class="[ | ||
| 'max-w-[80%] rounded-xl px-4 py-3 text-sm shadow-sm', | ||
| role === 'user' ? 'bg-slate-900 text-slate-50' : 'bg-slate-100 text-slate-900', | ||
| ]" | ||
| > | ||
| {{ content }} | ||
| </div> | ||
| </div> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <script setup lang="ts"> | ||
| import { cva, type VariantProps } from "class-variance-authority"; | ||
| import { computed } from "vue"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| const buttonVariants = cva( | ||
| "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-white", | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90", | ||
| ghost: "hover:bg-slate-100 hover:text-slate-900", | ||
| }, | ||
| size: { | ||
| default: "h-10 px-4 py-2", | ||
| sm: "h-9 rounded-md px-3", | ||
| icon: "h-10 w-10", | ||
| }, | ||
| }, | ||
| defaultVariants: { | ||
| variant: "default", | ||
| size: "default", | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| type ButtonVariants = VariantProps<typeof buttonVariants>; | ||
|
|
||
| const props = defineProps<{ | ||
| class?: string; | ||
| variant?: ButtonVariants["variant"]; | ||
| size?: ButtonVariants["size"]; | ||
| type?: "button" | "submit" | "reset"; | ||
| }>(); | ||
|
|
||
| const _classes = computed(() => | ||
| cn(buttonVariants({ variant: props.variant, size: props.size }), props.class) | ||
| ); | ||
| </script> | ||
|
|
||
| <template> | ||
| <button :type="type ?? 'button'" :class="classes"> | ||
| <slot /> | ||
| </button> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <script setup lang="ts"> | ||
| import { computed } from "vue"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| const props = defineProps<{ | ||
| class?: string; | ||
| modelValue?: string; | ||
| placeholder?: string; | ||
| type?: string; | ||
| }>(); | ||
|
|
||
| const _emit = defineEmits<{ | ||
| "update:modelValue": [value: string]; | ||
| }>(); | ||
|
|
||
| const _classes = computed(() => | ||
| cn( | ||
| "flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", | ||
| props.class | ||
| ) | ||
| ); | ||
| </script> | ||
|
|
||
| <template> | ||
| <input | ||
| :class="classes" | ||
| :type="type ?? 'text'" | ||
| :value="modelValue" | ||
| :placeholder="placeholder" | ||
| @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" | ||
| /> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { type ClassValue, clsx } from "clsx"; | ||
| import { twMerge } from "tailwind-merge"; | ||
|
|
||
| export function cn(...inputs: ClassValue[]) { | ||
| return twMerge(clsx(inputs)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { createApp } from "vue"; | ||
|
|
||
| import App from "./App.vue"; | ||
| import "./style.css"; | ||
|
|
||
| createApp(App).mount("#app"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| @import "tailwindcss"; | ||
|
|
||
| :root { | ||
| font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | ||
| } | ||
|
|
||
| * { | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| body { | ||
| margin: 0; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "extends": "@vue/tsconfig/tsconfig.dom.json", | ||
| "compilerOptions": { | ||
| "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||
| "baseUrl": ".", | ||
| "paths": { | ||
| "@/*": ["./src/*"] | ||
| } | ||
| }, | ||
| "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "files": [], | ||
| "references": [ | ||
| { | ||
| "path": "./tsconfig.app.json" | ||
| }, | ||
| { | ||
| "path": "./tsconfig.node.json" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "extends": "@tsconfig/node22/tsconfig.json", | ||
| "compilerOptions": { | ||
| "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||
| "module": "ESNext", | ||
| "moduleResolution": "Bundler", | ||
| "types": ["node"] | ||
| }, | ||
| "include": ["vite.config.ts"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change adds new dependencies to the dashboard manifest, but
clients/web/pnpm-lock.yamlwas not updated (importers.apps/dashboardremains{}), so installs that enforce lockfile consistency will fail with an outdated lockfile error. In this repository that directly impacts both Gradle-driven web builds (clients/web/build.gradle.ktsusesinstall --frozen-lockfilewhen the lockfile exists) and CI workflows that run frozen installs, blocking the new dashboard package from being built.Useful? React with 👍 / 👎.