fix:merge conflicts
This commit is contained in:
commit
97a164f682
8 changed files with 1418 additions and 1 deletions
12
src/App.tsx
12
src/App.tsx
|
|
@ -142,6 +142,18 @@ function App() {
|
|||
<Route path="projects" element={<FeatureViewPage view="projects" />} />
|
||||
<Route path="parcels" element={<FeatureViewPage view="parcels" />} />
|
||||
|
||||
{/* Chat Playground Feature Views */}
|
||||
<Route path="playground" element={<FeatureViewPage view="playground" />} />
|
||||
<Route path="workflows" element={<FeatureViewPage view="workflows" />} />
|
||||
|
||||
{/* Automation Feature Views */}
|
||||
<Route path="definitions" element={<FeatureViewPage view="definitions" />} />
|
||||
<Route path="templates" element={<FeatureViewPage view="templates" />} />
|
||||
<Route path="logs" element={<FeatureViewPage view="logs" />} />
|
||||
|
||||
{/* Teams Bot Feature Views */}
|
||||
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
|
||||
|
||||
{/* Catch-all für unbekannte Sub-Pfade */}
|
||||
<Route path="*" element={<FeatureViewPage view="not-found" />} />
|
||||
</Route>
|
||||
|
|
|
|||
183
src/api/teamsbotApi.ts
Normal file
183
src/api/teamsbotApi.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import api from '../api';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES & INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
export interface TeamsbotSession {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
mandateId: string;
|
||||
meetingLink: string;
|
||||
botName: string;
|
||||
backgroundImageUrl?: string;
|
||||
status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error';
|
||||
startedAt?: string;
|
||||
endedAt?: string;
|
||||
startedByUserId: string;
|
||||
bridgeSessionId?: string;
|
||||
meetingChatId?: string;
|
||||
summary?: string;
|
||||
errorMessage?: string;
|
||||
transcriptSegmentCount: number;
|
||||
botResponseCount: number;
|
||||
creationDate?: string;
|
||||
lastModified?: string;
|
||||
}
|
||||
|
||||
export interface TeamsbotTranscript {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
speaker?: string;
|
||||
text: string;
|
||||
timestamp: string;
|
||||
confidence: number;
|
||||
language?: string;
|
||||
isFinal: boolean;
|
||||
}
|
||||
|
||||
export interface TeamsbotBotResponse {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
responseText: string;
|
||||
responseType: 'audio' | 'chat' | 'both';
|
||||
detectedIntent: 'addressed' | 'question' | 'proactive' | 'none';
|
||||
reasoning?: string;
|
||||
triggeredByTranscriptId?: string;
|
||||
modelName?: string;
|
||||
processingTime: number;
|
||||
priceCHF: number;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface TeamsbotConfig {
|
||||
botName: string;
|
||||
backgroundImageUrl?: string;
|
||||
aiSystemPrompt: string;
|
||||
responseMode: 'auto' | 'manual' | 'transcribeOnly';
|
||||
language: string;
|
||||
voiceId?: string;
|
||||
bridgeUrl?: string;
|
||||
triggerIntervalSeconds: number;
|
||||
triggerCooldownSeconds: number;
|
||||
contextWindowSegments: number;
|
||||
}
|
||||
|
||||
export interface TeamsbotSessionStats {
|
||||
transcriptSegments: number;
|
||||
botResponses: number;
|
||||
totalCostCHF: number;
|
||||
totalProcessingTime: number;
|
||||
speakers: string[];
|
||||
}
|
||||
|
||||
export interface StartSessionRequest {
|
||||
meetingLink: string;
|
||||
botName?: string;
|
||||
backgroundImageUrl?: string;
|
||||
connectionId?: string;
|
||||
}
|
||||
|
||||
export interface ConfigUpdateRequest {
|
||||
botName?: string;
|
||||
backgroundImageUrl?: string;
|
||||
aiSystemPrompt?: string;
|
||||
responseMode?: 'auto' | 'manual' | 'transcribeOnly';
|
||||
language?: string;
|
||||
voiceId?: string;
|
||||
bridgeUrl?: string;
|
||||
triggerIntervalSeconds?: number;
|
||||
triggerCooldownSeconds?: number;
|
||||
contextWindowSegments?: number;
|
||||
}
|
||||
|
||||
// SSE Event Types
|
||||
export interface TeamsbotSSEEvent {
|
||||
type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState';
|
||||
data: any;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Start a new Teams Bot session.
|
||||
*/
|
||||
export async function startSession(instanceId: string, request: StartSessionRequest): Promise<{ session: TeamsbotSession }> {
|
||||
const response = await api.post(`/api/teamsbot/${instanceId}/sessions`, request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all sessions for a feature instance.
|
||||
*/
|
||||
export async function listSessions(instanceId: string, includeEnded = true): Promise<{ sessions: TeamsbotSession[] }> {
|
||||
const response = await api.get(`/api/teamsbot/${instanceId}/sessions`, {
|
||||
params: { includeEnded },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session details with transcripts and bot responses.
|
||||
*/
|
||||
export async function getSession(
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
includeTranscripts = true,
|
||||
includeResponses = true,
|
||||
): Promise<{
|
||||
session: TeamsbotSession;
|
||||
transcripts?: TeamsbotTranscript[];
|
||||
botResponses?: TeamsbotBotResponse[];
|
||||
stats?: TeamsbotSessionStats;
|
||||
}> {
|
||||
const response = await api.get(`/api/teamsbot/${instanceId}/sessions/${sessionId}`, {
|
||||
params: { includeTranscripts, includeResponses },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop an active session.
|
||||
*/
|
||||
export async function stopSession(instanceId: string, sessionId: string): Promise<{ status: string; sessionId: string }> {
|
||||
const response = await api.post(`/api/teamsbot/${instanceId}/sessions/${sessionId}/stop`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session and all related data.
|
||||
*/
|
||||
export async function deleteSession(instanceId: string, sessionId: string): Promise<{ deleted: boolean }> {
|
||||
const response = await api.delete(`/api/teamsbot/${instanceId}/sessions/${sessionId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get teamsbot configuration.
|
||||
*/
|
||||
export async function getConfig(instanceId: string): Promise<{ config: TeamsbotConfig }> {
|
||||
const response = await api.get(`/api/teamsbot/${instanceId}/config`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update teamsbot configuration.
|
||||
*/
|
||||
export async function updateConfig(instanceId: string, updates: ConfigUpdateRequest): Promise<{ config: TeamsbotConfig }> {
|
||||
const response = await api.put(`/api/teamsbot/${instanceId}/config`, updates);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an SSE EventSource for live session streaming.
|
||||
* Returns the EventSource instance for the caller to manage.
|
||||
*/
|
||||
export function createSessionStream(instanceId: string, sessionId: string): EventSource {
|
||||
const baseUrl = api.defaults.baseURL || '';
|
||||
const url = `${baseUrl}/api/teamsbot/${instanceId}/sessions/${sessionId}/stream`;
|
||||
return new EventSource(url, { withCredentials: true });
|
||||
}
|
||||
|
|
@ -20,7 +20,8 @@ import {
|
|||
FaCubes, FaEnvelopeOpenText, FaKey, FaUsersCog, FaCube, FaShieldAlt,
|
||||
FaLightbulb, FaRegFileAlt, FaLink, FaComments,
|
||||
FaListAlt, FaCogs, FaChartLine, FaFileAlt, FaUserShield, FaDatabase,
|
||||
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock
|
||||
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock,
|
||||
FaHeadset, FaVideo,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -79,6 +80,11 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'page.feature.realestate.projects': <FaProjectDiagram />,
|
||||
'page.feature.realestate.parcels': <FaMapMarkedAlt />,
|
||||
|
||||
// Feature pages - Teams Bot
|
||||
'page.feature.teamsbot.dashboard': <FaChartLine />,
|
||||
'page.feature.teamsbot.sessions': <FaVideo />,
|
||||
'page.feature.teamsbot.settings': <FaCog />,
|
||||
|
||||
// Feature icons (for feature grouping in navigation)
|
||||
'feature.trustee': <FaBriefcase />,
|
||||
'feature.realestate': <FaBuilding />,
|
||||
|
|
@ -86,6 +92,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'feature.chatplayground': <FaPlay />,
|
||||
'feature.automation': <FaCogs />,
|
||||
'feature.chatbot': <FaComments />,
|
||||
'feature.teamsbot': <FaHeadset />,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsVi
|
|||
// RealEstate Views
|
||||
import { RealEstatePekView, RealEstateProjectsView, RealEstateParcelsView, RealEstateInstanceRolesPlaceholder } from './views/realestate';
|
||||
|
||||
// Chat Playground Views (reusing existing workflow pages)
|
||||
import { PlaygroundPage, WorkflowsPage } from './workflows';
|
||||
|
||||
// Automation Views (reusing existing workflow pages)
|
||||
import { AutomationsPage, AutomationTemplatesPage } from './workflows';
|
||||
|
||||
// Teamsbot Views
|
||||
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
|
||||
import { TeamsbotSessionView } from './views/teamsbot/TeamsbotSessionView';
|
||||
import { TeamsbotSettingsView } from './views/teamsbot/TeamsbotSettingsView';
|
||||
|
||||
import styles from './FeatureView.module.css';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -103,6 +114,20 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
|||
parcels: RealEstateParcelsView,
|
||||
'instance-roles': RealEstateInstanceRolesPlaceholder,
|
||||
},
|
||||
chatplayground: {
|
||||
playground: PlaygroundPage,
|
||||
workflows: WorkflowsPage,
|
||||
},
|
||||
automation: {
|
||||
definitions: AutomationsPage,
|
||||
templates: AutomationTemplatesPage,
|
||||
logs: () => <PlaceholderView title="Execution Logs" description="Automatisierungs-Ausführungsprotokolle" />,
|
||||
},
|
||||
teamsbot: {
|
||||
dashboard: TeamsbotDashboardView,
|
||||
sessions: TeamsbotSessionView,
|
||||
settings: TeamsbotSettingsView,
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
497
src/pages/views/teamsbot/Teamsbot.module.css
Normal file
497
src/pages/views/teamsbot/Teamsbot.module.css
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
/* ============================================================================
|
||||
Teamsbot Feature Styles
|
||||
============================================================================ */
|
||||
|
||||
/* Dashboard */
|
||||
.dashboardContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.startSessionCard {
|
||||
background: var(--surface-color, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.formRow .formGroup {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary, #999);
|
||||
}
|
||||
|
||||
.input,
|
||||
.select,
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text-primary, #333);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.select:focus,
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #4A90D9);
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.startButton,
|
||||
.saveButton {
|
||||
padding: 0.6rem 1.5rem;
|
||||
background: var(--primary-color, #4A90D9);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.startButton:hover,
|
||||
.saveButton:hover {
|
||||
background: var(--primary-hover, #3A7BC8);
|
||||
}
|
||||
|
||||
.startButton:disabled,
|
||||
.saveButton:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stopButton {
|
||||
padding: 0.4rem 1rem;
|
||||
background: var(--danger-color, #D94A4A);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stopButton:hover {
|
||||
background: var(--danger-hover, #C83A3A);
|
||||
}
|
||||
|
||||
.viewButton {
|
||||
padding: 0.4rem 1rem;
|
||||
background: var(--surface-color, #f5f5f5);
|
||||
color: var(--primary-color, #4A90D9);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
padding: 0.4rem 1rem;
|
||||
background: transparent;
|
||||
color: var(--danger-color, #D94A4A);
|
||||
border: 1px solid var(--danger-color, #D94A4A);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Error/Success Banners */
|
||||
.errorBanner {
|
||||
background: rgba(217, 74, 74, 0.1);
|
||||
color: var(--danger-color, #D94A4A);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
border-left: 3px solid var(--danger-color, #D94A4A);
|
||||
}
|
||||
|
||||
.successBanner {
|
||||
background: rgba(74, 217, 154, 0.1);
|
||||
color: var(--success-color, #2D8E5C);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
border-left: 3px solid var(--success-color, #2D8E5C);
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.sectionContainer {
|
||||
background: var(--surface-color, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
/* Session List */
|
||||
.sessionList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sessionCard {
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
background: var(--surface-alt, #fafafa);
|
||||
}
|
||||
|
||||
.sessionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sessionBotName {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sessionMeta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary, #666);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sessionActions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.sessionSummary {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary, #666);
|
||||
background: var(--surface-color, #fff);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sessionError {
|
||||
font-size: 0.85rem;
|
||||
color: var(--danger-color, #D94A4A);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.statusBadge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.statusActive {
|
||||
background: rgba(74, 217, 154, 0.15);
|
||||
color: #2D8E5C;
|
||||
}
|
||||
|
||||
.statusJoining,
|
||||
.statusPending {
|
||||
background: rgba(74, 144, 217, 0.15);
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
.statusEnded {
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.statusError {
|
||||
background: rgba(217, 74, 74, 0.15);
|
||||
color: #D94A4A;
|
||||
}
|
||||
|
||||
.statusLeaving {
|
||||
background: rgba(217, 168, 74, 0.15);
|
||||
color: #B8860B;
|
||||
}
|
||||
|
||||
.liveBadge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: #D94A4A;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
color: var(--text-tertiary, #999);
|
||||
font-size: 0.9rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Session View
|
||||
============================================================================ */
|
||||
|
||||
.sessionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sessionViewHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--surface-color, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sessionInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sessionTitle {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.sessionControls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sessionContent {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Transcript Panel */
|
||||
.transcriptPanel,
|
||||
.responsesPanel {
|
||||
background: var(--surface-color, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
background: var(--surface-alt, #fafafa);
|
||||
}
|
||||
|
||||
.transcriptList,
|
||||
.responseList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.transcriptItem {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.transcriptItem:hover {
|
||||
background: var(--surface-alt, #f5f5f5);
|
||||
}
|
||||
|
||||
.transcriptTime {
|
||||
color: var(--text-tertiary, #999);
|
||||
font-size: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.transcriptSpeaker {
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.transcriptText {
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
/* Response Items */
|
||||
.responseItem {
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--surface-alt, #fafafa);
|
||||
}
|
||||
|
||||
.responseHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.responseIntent {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary-color, #4A90D9);
|
||||
}
|
||||
|
||||
.responseTime {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary, #999);
|
||||
}
|
||||
|
||||
.responseText {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.responseReasoning {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.responseMeta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary, #999);
|
||||
}
|
||||
|
||||
/* Summary Card */
|
||||
.summaryCard {
|
||||
background: var(--surface-color, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summaryText {
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary, #333);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Settings View
|
||||
============================================================================ */
|
||||
|
||||
.settingsContainer {
|
||||
padding: 1rem;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.settingsCard {
|
||||
background: var(--surface-color, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.settingsSection:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settingsActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
228
src/pages/views/teamsbot/TeamsbotDashboardView.tsx
Normal file
228
src/pages/views/teamsbot/TeamsbotDashboardView.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotSession, StartSessionRequest } from '../../../api/teamsbotApi';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
/**
|
||||
* TeamsbotDashboardView - Overview of all Teams Bot sessions.
|
||||
* Allows starting new sessions and viewing active/past sessions.
|
||||
*/
|
||||
export const TeamsbotDashboardView: React.FC = () => {
|
||||
const { instance } = useCurrentInstance();
|
||||
const instanceId = instance?.id || '';
|
||||
|
||||
const [sessions, setSessions] = useState<TeamsbotSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// New session form
|
||||
const [meetingLink, setMeetingLink] = useState('');
|
||||
const [botName, setBotName] = useState('');
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
|
||||
const _loadSessions = useCallback(async () => {
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await teamsbotApi.listSessions(instanceId);
|
||||
setSessions(result.sessions || []);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Laden der Sitzungen');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [instanceId]);
|
||||
|
||||
useEffect(() => {
|
||||
_loadSessions();
|
||||
}, [_loadSessions]);
|
||||
|
||||
const _handleStartSession = async () => {
|
||||
if (!meetingLink.trim()) return;
|
||||
|
||||
setIsStarting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const request: StartSessionRequest = {
|
||||
meetingLink: meetingLink.trim(),
|
||||
botName: botName.trim() || undefined,
|
||||
};
|
||||
|
||||
await teamsbotApi.startSession(instanceId, request);
|
||||
setMeetingLink('');
|
||||
setBotName('');
|
||||
await _loadSessions();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Starten der Sitzung');
|
||||
} finally {
|
||||
setIsStarting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleStopSession = async (sessionId: string) => {
|
||||
try {
|
||||
await teamsbotApi.stopSession(instanceId, sessionId);
|
||||
await _loadSessions();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Stoppen der Sitzung');
|
||||
}
|
||||
};
|
||||
|
||||
const _handleDeleteSession = async (sessionId: string) => {
|
||||
try {
|
||||
await teamsbotApi.deleteSession(instanceId, sessionId);
|
||||
await _loadSessions();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Loeschen der Sitzung');
|
||||
}
|
||||
};
|
||||
|
||||
const _getStatusBadgeClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return styles.statusActive;
|
||||
case 'joining': return styles.statusJoining;
|
||||
case 'pending': return styles.statusPending;
|
||||
case 'ended': return styles.statusEnded;
|
||||
case 'error': return styles.statusError;
|
||||
case 'leaving': return styles.statusLeaving;
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
const _getStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
pending: 'Wartend',
|
||||
joining: 'Beitritt...',
|
||||
active: 'Aktiv',
|
||||
leaving: 'Verlassen...',
|
||||
ended: 'Beendet',
|
||||
error: 'Fehler',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const activeSessions = sessions.filter(s => ['pending', 'joining', 'active'].includes(s.status));
|
||||
const pastSessions = sessions.filter(s => ['ended', 'error', 'leaving'].includes(s.status));
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardContainer}>
|
||||
{/* Start New Session Card */}
|
||||
<div className={styles.startSessionCard}>
|
||||
<h3 className={styles.cardTitle}>Neue Bot-Sitzung starten</h3>
|
||||
<p className={styles.cardDescription}>
|
||||
Fuege den Teams Meeting-Link ein, um den AI-Bot in ein Meeting einzuschleusen.
|
||||
</p>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Teams Meeting-Link *</label>
|
||||
<input
|
||||
type="url"
|
||||
className={styles.input}
|
||||
placeholder="https://teams.microsoft.com/l/meetup-join/..."
|
||||
value={meetingLink}
|
||||
onChange={(e) => setMeetingLink(e.target.value)}
|
||||
disabled={isStarting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Bot-Name (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder="AI Assistant"
|
||||
value={botName}
|
||||
onChange={(e) => setBotName(e.target.value)}
|
||||
disabled={isStarting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.startButton}
|
||||
onClick={_handleStartSession}
|
||||
disabled={isStarting || !meetingLink.trim()}
|
||||
>
|
||||
{isStarting ? 'Wird gestartet...' : 'Bot ins Meeting senden'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className={styles.errorBanner}>{error}</div>}
|
||||
|
||||
{/* Active Sessions */}
|
||||
{activeSessions.length > 0 && (
|
||||
<div className={styles.sectionContainer}>
|
||||
<h3 className={styles.sectionTitle}>Aktive Sitzungen</h3>
|
||||
<div className={styles.sessionList}>
|
||||
{activeSessions.map((session) => (
|
||||
<div key={session.id} className={styles.sessionCard}>
|
||||
<div className={styles.sessionHeader}>
|
||||
<span className={styles.sessionBotName}>{session.botName}</span>
|
||||
<span className={`${styles.statusBadge} ${_getStatusBadgeClass(session.status)}`}>
|
||||
{_getStatusLabel(session.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.sessionMeta}>
|
||||
<span>{session.transcriptSegmentCount} Segmente</span>
|
||||
<span>{session.botResponseCount} Antworten</span>
|
||||
{session.startedAt && <span>Seit: {new Date(session.startedAt).toLocaleTimeString('de-CH')}</span>}
|
||||
</div>
|
||||
<div className={styles.sessionActions}>
|
||||
<a href={`sessions?sessionId=${session.id}`} className={styles.viewButton}>Live ansehen</a>
|
||||
{session.status === 'active' && (
|
||||
<button className={styles.stopButton} onClick={() => _handleStopSession(session.id)}>
|
||||
Stoppen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Past Sessions */}
|
||||
<div className={styles.sectionContainer}>
|
||||
<h3 className={styles.sectionTitle}>
|
||||
{loading ? 'Lade Sitzungen...' : `Vergangene Sitzungen (${pastSessions.length})`}
|
||||
</h3>
|
||||
{pastSessions.length === 0 && !loading && (
|
||||
<p className={styles.emptyState}>Noch keine vergangenen Sitzungen.</p>
|
||||
)}
|
||||
<div className={styles.sessionList}>
|
||||
{pastSessions.map((session) => (
|
||||
<div key={session.id} className={styles.sessionCard}>
|
||||
<div className={styles.sessionHeader}>
|
||||
<span className={styles.sessionBotName}>{session.botName}</span>
|
||||
<span className={`${styles.statusBadge} ${_getStatusBadgeClass(session.status)}`}>
|
||||
{_getStatusLabel(session.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.sessionMeta}>
|
||||
<span>{session.transcriptSegmentCount} Segmente</span>
|
||||
<span>{session.botResponseCount} Antworten</span>
|
||||
{session.startedAt && <span>{new Date(session.startedAt).toLocaleDateString('de-CH')}</span>}
|
||||
</div>
|
||||
{session.summary && (
|
||||
<div className={styles.sessionSummary}>{session.summary.substring(0, 200)}...</div>
|
||||
)}
|
||||
{session.errorMessage && (
|
||||
<div className={styles.sessionError}>{session.errorMessage}</div>
|
||||
)}
|
||||
<div className={styles.sessionActions}>
|
||||
<a href={`sessions?sessionId=${session.id}`} className={styles.viewButton}>Details</a>
|
||||
<button className={styles.deleteButton} onClick={() => _handleDeleteSession(session.id)}>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsbotDashboardView;
|
||||
225
src/pages/views/teamsbot/TeamsbotSessionView.tsx
Normal file
225
src/pages/views/teamsbot/TeamsbotSessionView.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotSession, TeamsbotTranscript, TeamsbotBotResponse, TeamsbotSSEEvent } from '../../../api/teamsbotApi';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
/**
|
||||
* TeamsbotSessionView - Live session view with real-time transcript and bot responses.
|
||||
*/
|
||||
export const TeamsbotSessionView: React.FC = () => {
|
||||
const { instance } = useCurrentInstance();
|
||||
const instanceId = instance?.id || '';
|
||||
const [searchParams] = useSearchParams();
|
||||
const sessionId = searchParams.get('sessionId') || '';
|
||||
|
||||
const [session, setSession] = useState<TeamsbotSession | null>(null);
|
||||
const [transcripts, setTranscripts] = useState<TeamsbotTranscript[]>([]);
|
||||
const [botResponses, setBotResponses] = useState<TeamsbotBotResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
|
||||
const transcriptEndRef = useRef<HTMLDivElement>(null);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
// Load session data
|
||||
const _loadSession = useCallback(async () => {
|
||||
if (!instanceId || !sessionId) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await teamsbotApi.getSession(instanceId, sessionId);
|
||||
setSession(result.session);
|
||||
setTranscripts(result.transcripts || []);
|
||||
setBotResponses(result.botResponses || []);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Laden der Sitzung');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [instanceId, sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
_loadSession();
|
||||
}, [_loadSession]);
|
||||
|
||||
// SSE Live Stream
|
||||
useEffect(() => {
|
||||
if (!instanceId || !sessionId || !session) return;
|
||||
if (!['active', 'joining', 'pending'].includes(session.status)) return;
|
||||
|
||||
const eventSource = teamsbotApi.createSessionStream(instanceId, sessionId);
|
||||
eventSourceRef.current = eventSource;
|
||||
setIsLive(true);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const sseEvent: TeamsbotSSEEvent = JSON.parse(event.data);
|
||||
|
||||
switch (sseEvent.type) {
|
||||
case 'transcript':
|
||||
setTranscripts(prev => [...prev, sseEvent.data as TeamsbotTranscript]);
|
||||
break;
|
||||
|
||||
case 'botResponse':
|
||||
setBotResponses(prev => [...prev, sseEvent.data as TeamsbotBotResponse]);
|
||||
break;
|
||||
|
||||
case 'statusChange':
|
||||
setSession(prev => prev ? { ...prev, status: sseEvent.data.status } : null);
|
||||
if (['ended', 'error'].includes(sseEvent.data.status)) {
|
||||
setIsLive(false);
|
||||
eventSource.close();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'analysis':
|
||||
// Debug info - could show in UI
|
||||
break;
|
||||
|
||||
case 'suggestedResponse':
|
||||
// Manual mode: show suggested response
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SSE parse error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = () => {
|
||||
setIsLive(false);
|
||||
};
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
setIsLive(false);
|
||||
};
|
||||
}, [instanceId, sessionId, session?.status]);
|
||||
|
||||
// Auto-scroll transcript
|
||||
useEffect(() => {
|
||||
transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [transcripts]);
|
||||
|
||||
const _handleStop = async () => {
|
||||
if (!instanceId || !sessionId) return;
|
||||
try {
|
||||
await teamsbotApi.stopSession(instanceId, sessionId);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const _formatTime = (timestamp: string) => {
|
||||
try {
|
||||
return new Date(timestamp).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const _getSpeakerColor = (speaker: string) => {
|
||||
const colors = ['#4A90D9', '#D94A4A', '#4AD99A', '#D9A84A', '#9A4AD9', '#4AD9D9'];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < speaker.length; i++) {
|
||||
hash = speaker.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
};
|
||||
|
||||
if (loading) return <div className={styles.loading}>Lade Sitzung...</div>;
|
||||
if (!session) return <div className={styles.errorBanner}>Sitzung nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.sessionContainer}>
|
||||
{/* Session Header */}
|
||||
<div className={styles.sessionViewHeader}>
|
||||
<div className={styles.sessionInfo}>
|
||||
<h3 className={styles.sessionTitle}>{session.botName}</h3>
|
||||
<span className={`${styles.statusBadge} ${styles[`status${session.status.charAt(0).toUpperCase() + session.status.slice(1)}`] || ''}`}>
|
||||
{session.status}
|
||||
</span>
|
||||
{isLive && <span className={styles.liveBadge}>LIVE</span>}
|
||||
</div>
|
||||
<div className={styles.sessionControls}>
|
||||
{['active', 'joining'].includes(session.status) && (
|
||||
<button className={styles.stopButton} onClick={_handleStop}>Sitzung beenden</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className={styles.errorBanner}>{error}</div>}
|
||||
|
||||
{/* Main Content: Transcript + Responses */}
|
||||
<div className={styles.sessionContent}>
|
||||
{/* Left: Transcript */}
|
||||
<div className={styles.transcriptPanel}>
|
||||
<h4 className={styles.panelTitle}>Transkript ({transcripts.length} Segmente)</h4>
|
||||
<div className={styles.transcriptList}>
|
||||
{transcripts.map((t) => (
|
||||
<div key={t.id} className={styles.transcriptItem}>
|
||||
<span className={styles.transcriptTime}>{_formatTime(t.timestamp)}</span>
|
||||
<span
|
||||
className={styles.transcriptSpeaker}
|
||||
style={{ color: _getSpeakerColor(t.speaker || 'Unknown') }}
|
||||
>
|
||||
{t.speaker || 'Unknown'}:
|
||||
</span>
|
||||
<span className={styles.transcriptText}>{t.text}</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={transcriptEndRef} />
|
||||
{transcripts.length === 0 && (
|
||||
<div className={styles.emptyState}>Noch kein Transkript vorhanden.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Bot Responses */}
|
||||
<div className={styles.responsesPanel}>
|
||||
<h4 className={styles.panelTitle}>Bot-Antworten ({botResponses.length})</h4>
|
||||
<div className={styles.responseList}>
|
||||
{botResponses.map((r) => (
|
||||
<div key={r.id} className={styles.responseItem}>
|
||||
<div className={styles.responseHeader}>
|
||||
<span className={styles.responseIntent}>{r.detectedIntent}</span>
|
||||
<span className={styles.responseTime}>{_formatTime(r.timestamp || '')}</span>
|
||||
</div>
|
||||
<div className={styles.responseText}>{r.responseText}</div>
|
||||
{r.reasoning && (
|
||||
<div className={styles.responseReasoning}>
|
||||
<em>Reasoning: {r.reasoning}</em>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.responseMeta}>
|
||||
<span>{r.modelName}</span>
|
||||
<span>{r.processingTime.toFixed(1)}s</span>
|
||||
<span>{r.priceCHF.toFixed(4)} CHF</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{botResponses.length === 0 && (
|
||||
<div className={styles.emptyState}>Noch keine Bot-Antworten.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary (for ended sessions) */}
|
||||
{session.summary && (
|
||||
<div className={styles.summaryCard}>
|
||||
<h4 className={styles.panelTitle}>Meeting-Zusammenfassung</h4>
|
||||
<div className={styles.summaryText}>{session.summary}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsbotSessionView;
|
||||
240
src/pages/views/teamsbot/TeamsbotSettingsView.tsx
Normal file
240
src/pages/views/teamsbot/TeamsbotSettingsView.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotConfig, ConfigUpdateRequest } from '../../../api/teamsbotApi';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
/**
|
||||
* TeamsbotSettingsView - Bot configuration for a feature instance.
|
||||
*/
|
||||
export const TeamsbotSettingsView: React.FC = () => {
|
||||
const { instance } = useCurrentInstance();
|
||||
const instanceId = instance?.id || '';
|
||||
|
||||
const [config, setConfig] = useState<TeamsbotConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState<ConfigUpdateRequest>({});
|
||||
|
||||
const _loadConfig = useCallback(async () => {
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await teamsbotApi.getConfig(instanceId);
|
||||
setConfig(result.config);
|
||||
setFormData(result.config);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Laden der Konfiguration');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [instanceId]);
|
||||
|
||||
useEffect(() => {
|
||||
_loadConfig();
|
||||
}, [_loadConfig]);
|
||||
|
||||
const _handleSave = async () => {
|
||||
if (!instanceId) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccessMsg(null);
|
||||
|
||||
try {
|
||||
const result = await teamsbotApi.updateConfig(instanceId, formData);
|
||||
setConfig(result.config);
|
||||
setFormData(result.config);
|
||||
setSuccessMsg('Konfiguration gespeichert');
|
||||
setTimeout(() => setSuccessMsg(null), 3000);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Speichern');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _updateField = (field: keyof ConfigUpdateRequest, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
if (loading) return <div className={styles.loading}>Lade Konfiguration...</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.settingsContainer}>
|
||||
<div className={styles.settingsCard}>
|
||||
<h3 className={styles.cardTitle}>Bot-Einstellungen</h3>
|
||||
|
||||
{error && <div className={styles.errorBanner}>{error}</div>}
|
||||
{successMsg && <div className={styles.successBanner}>{successMsg}</div>}
|
||||
|
||||
{/* Bot Identity */}
|
||||
<div className={styles.settingsSection}>
|
||||
<h4 className={styles.sectionTitle}>Bot-Identitaet</h4>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Bot-Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={formData.botName || ''}
|
||||
onChange={(e) => _updateField('botName', e.target.value)}
|
||||
placeholder="AI Assistant"
|
||||
/>
|
||||
<span className={styles.hint}>Wird als Teilnehmer-Name im Meeting angezeigt</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Hintergrundbild-URL</label>
|
||||
<input
|
||||
type="url"
|
||||
className={styles.input}
|
||||
value={formData.backgroundImageUrl || ''}
|
||||
onChange={(e) => _updateField('backgroundImageUrl', e.target.value)}
|
||||
placeholder="https://example.com/background.jpg"
|
||||
/>
|
||||
<span className={styles.hint}>Hintergrundbild fuer den Video-Feed des Bots</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Behavior */}
|
||||
<div className={styles.settingsSection}>
|
||||
<h4 className={styles.sectionTitle}>AI-Verhalten</h4>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>System-Prompt</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
value={formData.aiSystemPrompt || ''}
|
||||
onChange={(e) => _updateField('aiSystemPrompt', e.target.value)}
|
||||
rows={6}
|
||||
placeholder="Beschreibe, wie sich der Bot im Meeting verhalten soll..."
|
||||
/>
|
||||
<span className={styles.hint}>Instruktionen fuer den AI-Bot: Wann und wie soll er antworten?</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Antwort-Modus</label>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={formData.responseMode || 'auto'}
|
||||
onChange={(e) => _updateField('responseMode', e.target.value)}
|
||||
>
|
||||
<option value="auto">Automatisch - AI entscheidet selbst</option>
|
||||
<option value="manual">Manuell - Antworten muessen bestaetigt werden</option>
|
||||
<option value="transcribeOnly">Nur Transkription - Keine AI-Antworten</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Settings */}
|
||||
<div className={styles.settingsSection}>
|
||||
<h4 className={styles.sectionTitle}>Sprach-Einstellungen</h4>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Sprache</label>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={formData.language || 'de-DE'}
|
||||
onChange={(e) => _updateField('language', e.target.value)}
|
||||
>
|
||||
<option value="de-DE">Deutsch (Deutschland)</option>
|
||||
<option value="de-CH">Deutsch (Schweiz)</option>
|
||||
<option value="en-US">English (US)</option>
|
||||
<option value="en-GB">English (UK)</option>
|
||||
<option value="fr-FR">Francais</option>
|
||||
<option value="it-IT">Italiano</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Stimme (Voice ID)</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={formData.voiceId || ''}
|
||||
onChange={(e) => _updateField('voiceId', e.target.value)}
|
||||
placeholder="de-DE-Standard-A"
|
||||
/>
|
||||
<span className={styles.hint}>Google TTS Voice ID fuer die Sprachausgabe</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Settings */}
|
||||
<div className={styles.settingsSection}>
|
||||
<h4 className={styles.sectionTitle}>Erweiterte Einstellungen</h4>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Analyse-Intervall (Sek.)</label>
|
||||
<input
|
||||
type="number"
|
||||
className={styles.input}
|
||||
value={formData.triggerIntervalSeconds || 10}
|
||||
onChange={(e) => _updateField('triggerIntervalSeconds', parseInt(e.target.value))}
|
||||
min={3}
|
||||
max={60}
|
||||
/>
|
||||
<span className={styles.hint}>Periodisches Analyse-Intervall</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Cooldown (Sek.)</label>
|
||||
<input
|
||||
type="number"
|
||||
className={styles.input}
|
||||
value={formData.triggerCooldownSeconds || 3}
|
||||
onChange={(e) => _updateField('triggerCooldownSeconds', parseInt(e.target.value))}
|
||||
min={1}
|
||||
max={30}
|
||||
/>
|
||||
<span className={styles.hint}>Min. Abstand zwischen AI-Calls</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Kontext-Fenster (Segmente)</label>
|
||||
<input
|
||||
type="number"
|
||||
className={styles.input}
|
||||
value={formData.contextWindowSegments || 20}
|
||||
onChange={(e) => _updateField('contextWindowSegments', parseInt(e.target.value))}
|
||||
min={5}
|
||||
max={100}
|
||||
/>
|
||||
<span className={styles.hint}>Anzahl Transkript-Segmente fuer AI-Kontext</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Media Bridge URL</label>
|
||||
<input
|
||||
type="url"
|
||||
className={styles.input}
|
||||
value={formData.bridgeUrl || ''}
|
||||
onChange={(e) => _updateField('bridgeUrl', e.target.value)}
|
||||
placeholder="https://bridge.example.com"
|
||||
/>
|
||||
<span className={styles.hint}>URL des .NET Media Bridge Service</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className={styles.settingsActions}>
|
||||
<button
|
||||
className={styles.saveButton}
|
||||
onClick={_handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Speichern...' : 'Konfiguration speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsbotSettingsView;
|
||||
Loading…
Reference in a new issue