Skip to content

Commit e1e8466

Browse files
committed
feat: Issue #8 - Frontend Integration for AI Plugin System
Implement main project's AI interface components and API integration: Frontend Components: - AIStatusIndicator: Real-time AI plugin health monitoring with status badges - AITriggerButton: Floating action button for AI interface access - Integrated both components into main App.vue interface API Integration: - Extended net.ts with comprehensive AI plugin management endpoints: - DiscoverAIPlugins() for plugin discovery - CheckAIPluginHealth() for individual health monitoring - GetAllAIPluginHealth() for bulk health status retrieval - RegisterAIPlugin() and UnregisterAIPlugin() for lifecycle management - Added TypeScript interfaces for AIPluginInfo and AIPluginHealth User Interface Features: - AI status indicator in top menu bar with real-time plugin health - Floating action button positioned in bottom-right corner - Plugin status polling every 30 seconds for live updates - Accessibility support with ARIA labels and keyboard navigation - Responsive design with hover animations and processing states - Error handling with graceful fallbacks Test Coverage: - Comprehensive test suite for both AI components - Tests cover rendering, interaction, accessibility, and error scenarios - Proper mocking of API endpoints for isolated testing Architecture: This implementation provides the main project interfaces only, following the decoupled approach where actual AI functionality is implemented by separate AI plugins that integrate through the established plugin system. The frontend serves as a bridge between users and AI plugins, providing discovery, health monitoring, and interaction interfaces while keeping plugin-specific code in separate repositories.
1 parent 1eb067a commit e1e8466

File tree

5 files changed

+520
-1
lines changed

5 files changed

+520
-1
lines changed

console/atest-ui/src/App.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
Share,
77
ArrowDown,
88
Guide,
9-
Help, Setting
9+
Help, Setting,
10+
ChatLineSquare,
11+
Loading,
12+
CircleCheck,
13+
CircleClose,
14+
Warning,
15+
QuestionFilled
1016
} from '@element-plus/icons-vue'
1117
import * as ElementPlusIcons from '@element-plus/icons-vue'
1218
import { ref, watch, getCurrentInstance} from 'vue'
@@ -19,6 +25,8 @@ import StoreManager from './views/StoreManager.vue'
1925
import WelcomePage from './views/WelcomePage.vue'
2026
import MagicKey from './components/MagicKey.vue'
2127
import Extension from './views/Extension.vue'
28+
import AIStatusIndicator from './components/AIStatusIndicator.vue'
29+
import AITriggerButton from './components/AITriggerButton.vue'
2230
import { useI18n } from 'vue-i18n'
2331
import ElementPlus from 'element-plus';
2432
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
@@ -168,6 +176,7 @@ API.GetMenus((menus) => {
168176
<el-main class="center-zone">
169177
<div class="top-menu">
170178
<el-col style="display: flex; align-items: center;">
179+
<AIStatusIndicator />
171180
<el-icon @click="settingDialogVisible=true" size="20"><Setting /></el-icon>
172181
</el-col>
173182
</div>
@@ -237,6 +246,7 @@ API.GetMenus((menus) => {
237246
</el-dialog>
238247

239248
<MagicKey />
249+
<AITriggerButton />
240250
</template>
241251

242252
<style>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted, onUnmounted } from 'vue'
3+
import { API, AIPluginHealth } from '../views/net'
4+
5+
const aiPluginHealth = ref<Record<string, AIPluginHealth>>({})
6+
const loading = ref(true)
7+
const hasAIPlugins = ref(false)
8+
let healthCheckInterval: number | null = null
9+
10+
const loadAIPluginHealth = () => {
11+
API.GetAllAIPluginHealth(
12+
(healthData) => {
13+
aiPluginHealth.value = healthData
14+
hasAIPlugins.value = Object.keys(healthData).length > 0
15+
loading.value = false
16+
},
17+
() => {
18+
loading.value = false
19+
}
20+
)
21+
}
22+
23+
const getStatusColor = (status: string) => {
24+
switch (status) {
25+
case 'online':
26+
return 'success'
27+
case 'offline':
28+
return 'danger'
29+
case 'error':
30+
return 'danger'
31+
case 'processing':
32+
return 'warning'
33+
default:
34+
return 'info'
35+
}
36+
}
37+
38+
const getStatusIcon = (status: string) => {
39+
switch (status) {
40+
case 'online':
41+
return 'CircleCheck'
42+
case 'offline':
43+
return 'CircleClose'
44+
case 'error':
45+
return 'Warning'
46+
case 'processing':
47+
return 'Loading'
48+
default:
49+
return 'QuestionFilled'
50+
}
51+
}
52+
53+
onMounted(() => {
54+
loadAIPluginHealth()
55+
// Poll for health status every 30 seconds
56+
healthCheckInterval = window.setInterval(loadAIPluginHealth, 30000)
57+
})
58+
59+
onUnmounted(() => {
60+
if (healthCheckInterval) {
61+
clearInterval(healthCheckInterval)
62+
}
63+
})
64+
</script>
65+
66+
<template>
67+
<div class="ai-status-indicator" v-if="hasAIPlugins">
68+
<el-tooltip content="AI Plugin Status" placement="top">
69+
<div class="ai-status-container">
70+
<el-icon v-if="loading" class="is-loading">
71+
<Loading />
72+
</el-icon>
73+
<template v-else>
74+
<el-badge
75+
v-for="(health, name) in aiPluginHealth"
76+
:key="name"
77+
:type="getStatusColor(health.status)"
78+
:dot="true"
79+
class="ai-plugin-badge"
80+
>
81+
<el-tooltip
82+
:content="`${health.name}: ${health.status}${health.errorMessage ? ' - ' + health.errorMessage : ''}`"
83+
placement="top"
84+
>
85+
<el-icon :class="`status-${health.status}`">
86+
<component :is="getStatusIcon(health.status)" />
87+
</el-icon>
88+
</el-tooltip>
89+
</el-badge>
90+
</template>
91+
</div>
92+
</el-tooltip>
93+
</div>
94+
</template>
95+
96+
<style scoped>
97+
.ai-status-indicator {
98+
display: flex;
99+
align-items: center;
100+
margin-left: 10px;
101+
}
102+
103+
.ai-status-container {
104+
display: flex;
105+
align-items: center;
106+
gap: 4px;
107+
}
108+
109+
.ai-plugin-badge {
110+
display: inline-flex;
111+
align-items: center;
112+
}
113+
114+
.status-online {
115+
color: var(--el-color-success);
116+
}
117+
118+
.status-offline {
119+
color: var(--el-color-danger);
120+
}
121+
122+
.status-error {
123+
color: var(--el-color-danger);
124+
}
125+
126+
.status-processing {
127+
color: var(--el-color-warning);
128+
animation: spin 2s linear infinite;
129+
}
130+
131+
@keyframes spin {
132+
from { transform: rotate(0deg); }
133+
to { transform: rotate(360deg); }
134+
}
135+
</style>
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
5+
const { t } = useI18n()
6+
const isProcessing = ref(false)
7+
const showAIDialog = ref(false)
8+
9+
const emits = defineEmits(['ai-trigger-clicked'])
10+
11+
const handleAITrigger = () => {
12+
if (isProcessing.value) return
13+
14+
emits('ai-trigger-clicked')
15+
showAIDialog.value = true
16+
}
17+
18+
const closeDialog = () => {
19+
showAIDialog.value = false
20+
}
21+
22+
// For demo purposes - in real implementation, this would be managed by parent
23+
const simulateProcessing = () => {
24+
isProcessing.value = true
25+
setTimeout(() => {
26+
isProcessing.value = false
27+
}, 3000)
28+
}
29+
</script>
30+
31+
<template>
32+
<div class="ai-trigger-container">
33+
<!-- Floating Action Button -->
34+
<el-button
35+
type="primary"
36+
circle
37+
size="large"
38+
class="ai-trigger-button"
39+
:class="{ 'is-processing': isProcessing }"
40+
:loading="isProcessing"
41+
@click="handleAITrigger"
42+
:aria-label="t ? t('ai.triggerButton') : 'AI Assistant'"
43+
tabindex="0"
44+
>
45+
<el-icon v-if="!isProcessing" size="24">
46+
<ChatLineSquare />
47+
</el-icon>
48+
</el-button>
49+
50+
<!-- Simple AI Dialog Interface -->
51+
<el-dialog
52+
v-model="showAIDialog"
53+
title="AI Assistant"
54+
width="60%"
55+
:before-close="closeDialog"
56+
destroy-on-close
57+
>
58+
<div class="ai-dialog-content">
59+
<el-alert
60+
title="AI Plugin Interface"
61+
type="info"
62+
:closable="false"
63+
show-icon
64+
>
65+
<template #default>
66+
This is the main project's AI interface. Actual AI functionality
67+
should be implemented by separate AI plugins that integrate with
68+
this interface through the plugin system.
69+
</template>
70+
</el-alert>
71+
72+
<div class="ai-placeholder">
73+
<p>AI plugins can be loaded here dynamically...</p>
74+
<el-button @click="simulateProcessing" :disabled="isProcessing">
75+
Simulate AI Processing
76+
</el-button>
77+
</div>
78+
</div>
79+
80+
<template #footer>
81+
<div class="dialog-footer">
82+
<el-button @click="closeDialog">Close</el-button>
83+
</div>
84+
</template>
85+
</el-dialog>
86+
</div>
87+
</template>
88+
89+
<style scoped>
90+
.ai-trigger-container {
91+
position: fixed;
92+
bottom: 30px;
93+
right: 30px;
94+
z-index: 1000;
95+
}
96+
97+
.ai-trigger-button {
98+
width: 60px;
99+
height: 60px;
100+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
101+
transition: all 0.3s ease;
102+
border: none;
103+
background: linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3));
104+
}
105+
106+
.ai-trigger-button:hover {
107+
transform: translateY(-2px);
108+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
109+
}
110+
111+
.ai-trigger-button:active {
112+
transform: translateY(0);
113+
}
114+
115+
.ai-trigger-button.is-processing {
116+
animation: pulse 2s infinite;
117+
}
118+
119+
@keyframes pulse {
120+
0% {
121+
box-shadow: 0 0 0 0 rgba(var(--el-color-primary-rgb), 0.7);
122+
}
123+
70% {
124+
box-shadow: 0 0 0 10px rgba(var(--el-color-primary-rgb), 0);
125+
}
126+
100% {
127+
box-shadow: 0 0 0 0 rgba(var(--el-color-primary-rgb), 0);
128+
}
129+
}
130+
131+
.ai-dialog-content {
132+
padding: 20px 0;
133+
}
134+
135+
.ai-placeholder {
136+
margin-top: 20px;
137+
text-align: center;
138+
padding: 40px;
139+
border: 2px dashed var(--el-border-color);
140+
border-radius: 8px;
141+
color: var(--el-text-color-secondary);
142+
}
143+
144+
/* Accessibility improvements */
145+
.ai-trigger-button:focus {
146+
outline: 2px solid var(--el-color-primary);
147+
outline-offset: 2px;
148+
}
149+
150+
@media (prefers-reduced-motion: reduce) {
151+
.ai-trigger-button {
152+
transition: none;
153+
}
154+
155+
.ai-trigger-button:hover {
156+
transform: none;
157+
}
158+
159+
.ai-trigger-button.is-processing {
160+
animation: none;
161+
}
162+
}
163+
</style>

0 commit comments

Comments
 (0)