/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { Message } from '../pojo/Message'; import { LocalStorageService } from './local-storage.service'; export interface ChatMessage { content: string; role: 'user' | 'assistant' | 'system_push'; gmtCreate: Date; securityForm: SecurityForm; /** Flag indicating this message is a skill report that should be displayed directly */ isSkillReport?: boolean; } export interface SecurityForm { show: Boolean; param: string; content: string; complete: boolean; } export const DEFAULT_SECURITY_FORM: SecurityForm = { show: false, param: '', content: '', complete: false }; export interface ChatConversation { id: number; title: string; gmtCreated: Date; gmtUpdate: Date; messages: ChatMessage[]; } export interface ChatRequestContext { message: string; conversationId?: number; } export interface SopSchedule { id?: number; conversationId: number; sopName: string; sopParams?: string; cronExpression: string; enabled: boolean; lastRunTime?: Date; nextRunTime?: Date; gmtCreate?: Date; gmtUpdate?: Date; } export interface SkillInfo { name: string; description: string; } const chat_uri = '/chat'; const schedule_uri = '/ai/schedule'; @Injectable({ providedIn: 'root' }) export class AiChatService { constructor(private http: HttpClient, private localStorageService: LocalStorageService) {} /** * Create a new conversation */ createConversation(): Observable> { return this.http.post>(`${chat_uri}/conversations`, {}); } /** * Get all conversations */ getConversations(): Observable> { return this.http.get>(`${chat_uri}/conversations`); } /** * Get a specific conversation with its history */ getConversation(conversationId: number): Observable> { return this.http.get>(`${chat_uri}/conversations/${conversationId}`); } /** * Delete a conversation */ deleteConversation(conversationId: number): Observable> { return this.http.delete>(`${chat_uri}/conversations/${conversationId}`); } /** * Send a message and get streaming response */ streamChat(message: string, conversationId?: number): Observable { const responseSubject = new Subject(); const requestBody: ChatRequestContext = { message }; if (conversationId) { requestBody.conversationId = conversationId; } const token = this.localStorageService.getAuthorizationToken(); const headers: Record = { 'Content-Type': 'application/json', Accept: 'text/event-stream', 'Cache-Control': 'no-cache' }; // Add Authorization header like the interceptor does if (token) { headers['Authorization'] = `Bearer ${token}`; } // Use fetch for SSE streaming (HttpClient doesn't support true streaming) fetch(`/api${chat_uri}/stream`, { method: 'POST', headers, body: JSON.stringify(requestBody) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('No reader available'); } const decoder = new TextDecoder(); let buffer = ''; const readStream = (): Promise => { if (!reader) { return Promise.resolve(); } return reader.read().then(({ value, done }) => { if (done) { responseSubject.complete(); return; } const chunk = decoder.decode(value, { stream: true }); buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('data:')) { const jsonStr = trimmedLine.substring(5).trim(); if (jsonStr && jsonStr !== '[DONE]') { try { const data = JSON.parse(jsonStr); if (data.response !== undefined) { responseSubject.next({ content: data.response || '', role: 'assistant', securityForm: DEFAULT_SECURITY_FORM, gmtCreate: data.timestamp ? new Date(data.timestamp) : new Date() }); } } catch (parseError) { console.error('Error parsing SSE data:', parseError, 'Raw data:', jsonStr); if (jsonStr) { responseSubject.next({ content: jsonStr, role: 'assistant', securityForm: DEFAULT_SECURITY_FORM, gmtCreate: new Date() }); } } } } } return readStream(); }); }; return readStream(); }) .catch(error => { console.error('Chat stream error:', error); responseSubject.error(error); }); return responseSubject.asObservable(); } saveSecurityData(body: any): Observable> { return this.http.post>(`${chat_uri}/security`, body); } // ===== Schedule API Methods ===== /** * Get all schedules for a conversation */ getSchedules(conversationId: number): Observable> { return this.http.get>(`${schedule_uri}/conversation/${conversationId}`); } /** * Create a new schedule */ createSchedule(schedule: SopSchedule): Observable> { return this.http.post>(schedule_uri, schedule); } /** * Update a schedule */ updateSchedule(id: number, schedule: SopSchedule): Observable> { return this.http.put>(`${schedule_uri}/${id}`, schedule); } /** * Delete a schedule */ deleteSchedule(id: number): Observable> { return this.http.delete>(`${schedule_uri}/${id}`); } /** * Toggle schedule enabled status */ toggleSchedule(id: number, enabled: boolean): Observable> { return this.http.put>(`${schedule_uri}/${id}/toggle?enabled=${enabled}`, {}); } /** * Get available SOP skills */ getAvailableSkills(): Observable> { return this.http.get>(`${schedule_uri}/skills`); } }