fix(shadcn): isolate Nexus CSS vars with --nx- prefix + admin password reset endpoint
This commit is contained in:
@@ -1,15 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { X, ExternalLink } from '@lucide/vue'
|
||||
import type { AgentNodeData } from '../../composables/useDashboardData'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import Select from '@/components/ui/Select.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
agent: AgentNodeData
|
||||
runtime: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
interface ModelOption {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
}
|
||||
|
||||
const availableModels = ref<ModelOption[]>([])
|
||||
const selectedModel = ref('')
|
||||
const currentModel = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/models', { credentials: 'include' })
|
||||
if (res.ok) {
|
||||
availableModels.value = await res.json()
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCurrentModel() {
|
||||
try {
|
||||
const res = await fetch(`/api/dashboard/agents/${props.agent.id}/model`, { credentials: 'include' })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
selectedModel.value = data.model
|
||||
currentModel.value = data.model
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
|
||||
async function saveModel() {
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/dashboard/agents/${props.agent.id}/model`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: selectedModel.value }),
|
||||
})
|
||||
if (res.ok) {
|
||||
currentModel.value = selectedModel.value
|
||||
toast.success('Model updated successfully')
|
||||
} else {
|
||||
toast.error('Failed to update model')
|
||||
}
|
||||
} catch {
|
||||
toast.error('Connection error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadModels()
|
||||
await loadCurrentModel()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -30,47 +99,56 @@ defineEmits<{
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close-btn" @click="$emit('close')" aria-label="Close">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="$emit('close')" aria-label="Close">
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Status</h3>
|
||||
<div class="status-row">
|
||||
<Badge
|
||||
:variant="agent.active ? 'default' : 'secondary'"
|
||||
:class="agent.active ? 'bg-[rgba(81,212,154,0.1)] text-[#51d49a] border-[rgba(81,212,154,0.3)]' : 'bg-[rgba(107,115,133,0.08)] text-[#6b7385] border-[rgba(107,115,133,0.2)]'"
|
||||
>
|
||||
<span class="status-dot" :class="{ active: agent.active }" />
|
||||
{{ agent.active ? 'Active' : 'Idle' }}
|
||||
</Badge>
|
||||
<span v-if="agent.active" class="footer-badge">Runtime: {{ runtime }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="modal-desc">{{ agent.description }}</p>
|
||||
|
||||
<!-- Tags -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Tags</h3>
|
||||
<div class="modal-tags-row">
|
||||
<Badge
|
||||
v-for="tag in agent.tags"
|
||||
:key="tag"
|
||||
variant="outline"
|
||||
:style="{ background: `${agent.color}18`, color: agent.color, borderColor: `${agent.color}30` }"
|
||||
>
|
||||
{{ tag }}
|
||||
</Badge>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Current Task -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Current Task</h3>
|
||||
<p class="section-value">
|
||||
{{ agent.currentTask }}
|
||||
<span class="thinking-dots">
|
||||
<span v-if="agent.active" class="thinking-dots">
|
||||
<span class="thinking-dot blue"></span>
|
||||
<span class="thinking-dot violet"></span>
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Live Thinking -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Live Thinking</h3>
|
||||
<div class="thinking-panel">
|
||||
<div class="thinking-stream">
|
||||
<div
|
||||
v-for="(msg, idx) in agent.thinkingStream"
|
||||
:key="idx"
|
||||
class="thinking-entry"
|
||||
:style="{ animationDelay: `${idx * 0.05}s` }"
|
||||
>
|
||||
<span class="entry-time">{{ msg.time }}</span>
|
||||
<span class="entry-text">{{ msg.text }}</span>
|
||||
</div>
|
||||
<div v-if="!agent.thinkingStream?.length" class="thinking-placeholder">
|
||||
Waiting for thought stream...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Goal + Progress -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Goal</h3>
|
||||
@@ -78,33 +156,38 @@ defineEmits<{
|
||||
<div class="progress-row">
|
||||
<span class="progress-pct">{{ agent.progress }}%</span>
|
||||
<div class="progress-track">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${agent.progress}%` }"
|
||||
></div>
|
||||
<div class="progress-fill" :style="{ width: `${agent.progress}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Working Feed -->
|
||||
<!-- Model -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Working Feed</h3>
|
||||
<div class="work-feed">
|
||||
<div
|
||||
v-for="(step, idx) in agent.workingFeed"
|
||||
:key="idx"
|
||||
class="work-step"
|
||||
<h3 class="section-label">Model</h3>
|
||||
<div class="model-select-row">
|
||||
<Select v-model="selectedModel" class="flex-1 text-xs border-[#a78bfa]">
|
||||
<option v-for="m in availableModels" :key="m.id" :value="m.id">
|
||||
{{ m.name }} ({{ m.provider }})
|
||||
</option>
|
||||
</Select>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
class="bg-[#a78bfa] hover:bg-[#c4b5fd]"
|
||||
:disabled="saving || selectedModel === currentModel"
|
||||
@click="saveModel"
|
||||
>
|
||||
<span class="step-dot"></span>
|
||||
<span class="step-time">{{ step.time }}</span>
|
||||
<span class="step-text">{{ step.text }}</span>
|
||||
</div>
|
||||
{{ saving ? 'Saving...' : 'Save' }}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer Stats -->
|
||||
<div class="modal-footer">
|
||||
<span class="footer-badge">Runtime: {{ runtime }}</span>
|
||||
<Badge :class="agent.active ? 'bg-[rgba(81,212,154,0.06)] text-[#51d49a] border-[rgba(81,212,154,0.25)]' : 'bg-[rgba(107,115,133,0.04)] text-[#6b7385] border-[rgba(107,115,133,0.15)]'">
|
||||
{{ agent.active ? '● Active' : '○ Idle' }}
|
||||
</Badge>
|
||||
<span v-if="agent.active" class="footer-badge">{{ runtime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,24 +296,6 @@ defineEmits<{
|
||||
color: var(--agent-color);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #6b7385;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-close-btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: #e8eaf0;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
@@ -261,6 +326,36 @@ defineEmits<{
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Status */
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #6b7385;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.status-dot.active {
|
||||
background: #51d49a;
|
||||
box-shadow: 0 0 8px rgba(81, 212, 154, 0.6);
|
||||
animation: pulse-dot 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.modal-tags-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Progress */
|
||||
.progress-row {
|
||||
display: flex;
|
||||
@@ -289,21 +384,7 @@ defineEmits<{
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* Live Thinking */
|
||||
.thinking-panel {
|
||||
position: relative;
|
||||
border: 1px solid rgba(139, 124, 246, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
background: rgba(12, 16, 22, 0.6);
|
||||
overflow: hidden;
|
||||
animation: panel-pulse 2.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes panel-pulse {
|
||||
0%, 100% { border-color: rgba(139, 124, 246, 0.25); box-shadow: 0 0 12px rgba(139,124,246,0.08); }
|
||||
50% { border-color: rgba(139, 124, 246, 0.5); box-shadow: 0 0 24px rgba(139,124,246,0.18), 0 0 40px rgba(139,124,246,0.06); }
|
||||
}
|
||||
|
||||
/* Thinking Dots */
|
||||
.thinking-dots {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
@@ -333,75 +414,6 @@ defineEmits<{
|
||||
50% { opacity: 1; transform: scale(1.4); }
|
||||
}
|
||||
|
||||
.thinking-stream {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.thinking-entry {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
animation: slide-in-right 0.3s ease-out both;
|
||||
font-size: 10px;
|
||||
}
|
||||
@keyframes slide-in-right {
|
||||
from { opacity: 0; transform: translateX(-16px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.entry-time {
|
||||
font-size: 8.5px;
|
||||
color: #6b7385;
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 42px;
|
||||
}
|
||||
.entry-text {
|
||||
color: #9ea5b3;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.thinking-placeholder {
|
||||
font-size: 10px;
|
||||
color: #4a5160;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Working Feed */
|
||||
.work-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.work-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.step-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--agent-color);
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.step-text {
|
||||
font-size: 10.5px;
|
||||
color: #7e8799;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.step-time {
|
||||
font-size: 8.5px;
|
||||
color: #6b7385;
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
@@ -419,4 +431,11 @@ defineEmits<{
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
color: #7e8799;
|
||||
}
|
||||
|
||||
/* Model Selector */
|
||||
.model-select-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user