Browse Source

feat: ai text-reponse & completion

未来全栈 1 day ago
parent
commit
c20321b412

+ 5 - 1
industry-monitor-web/src/app/app.config.ts

@@ -2,11 +2,15 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChang
 import { provideRouter } from '@angular/router';
 
 import { routes } from './app.routes';
+import { provideAnimations } from '@angular/platform-browser/animations';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
 
 export const appConfig: ApplicationConfig = {
   providers: [
     provideBrowserGlobalErrorListeners(),
     provideZoneChangeDetection({ eventCoalescing: true }),
-    provideRouter(routes)
+    provideRouter(routes),
+    provideAnimations(),
+    provideAnimationsAsync()
   ]
 };

+ 2 - 0
industry-monitor-web/src/app/app.routes.ts

@@ -7,6 +7,7 @@ import { DeviceMonitorComponent } from './pages/device-monitor/device-monitor.co
 import { DeviceManagementComponent } from './pages/device-management/device-management.component';
 import { HistoricalDataComponent } from './pages/historical-data/historical-data.component';
 import { SystemSettingsComponent } from './pages/system-settings/system-settings.component';
+import { PageTextResponse } from './pages/ai-suggestions/page-text-response/page-text-response';
 import { AiAssistantComponent } from './pages/ai-suggestions/ai-assistant.component';
 
 export const routes: Routes = [
@@ -21,6 +22,7 @@ export const routes: Routes = [
       { path: 'history', component: HistoricalDataComponent },
       { path: 'settings', component: SystemSettingsComponent },
       { path: 'aiassistant', component: AiAssistantComponent },
+      { path: 'aiassistant/text', component: PageTextResponse },
       { 
         path: 'device', 
         loadComponent: () => import('../modules/industry/machine/page-vibration-monitor/page-vibration-monitor').then(m => m.PageVibrationMonitorComponent)

+ 1 - 1
industry-monitor-web/src/app/pages/ai-suggestions/ai-assistant.component.html

@@ -9,7 +9,7 @@
 </div>
 
 <!-- Logo -->
-<div class="logo">
+<div class="logo" routerLink="/aiassistant/text">
   <i class="fas fa-robot logo-icon"></i>
   <div class="logo-text">智能设备诊断助手</div>
 </div>

+ 2 - 1
industry-monitor-web/src/app/pages/ai-suggestions/ai-assistant.component.ts

@@ -1,5 +1,6 @@
 import { CommonModule } from '@angular/common';
 import { Component, ElementRef, ViewChild, AfterViewInit, OnInit } from '@angular/core';
+import { RouterModule } from '@angular/router';
 import * as echarts from 'echarts';
 
 @Component({
@@ -7,7 +8,7 @@ import * as echarts from 'echarts';
   templateUrl: './ai-assistant.component.html',
   styleUrls: ['./ai-assistant.component.scss'],
   imports:[
-    CommonModule
+    CommonModule,RouterModule
   ],
   standalone: true
 })

+ 130 - 0
industry-monitor-web/src/app/pages/ai-suggestions/page-text-response/page-text-response.html

@@ -0,0 +1,130 @@
+<div class="chat-container">
+  <!-- 顶部导航栏 -->
+  <header class="chat-header">
+    <div class="header-content">
+      <div class="ai-identity">
+        <div class="avatar-pulse">
+          <div class="avatar">AI</div>
+          <div class="pulse-ring" [class.active]="isLoading()"></div>
+        </div>
+        <h1>DeepSeek Chat</h1>
+      </div>
+      <div class="header-actions">
+        <button class="icon-button" (click)="toggleHistory()" title="History">📜</button>
+        <div class="status-indicator" [class.error]="errorMessage()" [class.loading]="isLoading()">
+          <span>{{ errorMessage() || (isLoading() ? 'Thinking...' : 'Online') }}</span>
+          <div class="status-light"></div>
+        </div>
+      </div>
+    </div>
+  </header>
+
+  <!-- 对话展示区 -->
+  <div class="chat-messages" #messagesContainer>
+    @for (message of messageList(); track $index; let last = $last) {
+      <div class="message-wrapper" [class.new-group]="isNewGroup($index)">
+        <div class="message" 
+             [class.user]="message.role === 'user'" 
+             [class.assistant]="message.role === 'assistant'"
+             [@messageAnimation]>
+          <div class="message-content">
+            @if (message.role === 'assistant') {
+              <div class="avatar-container">
+                <div class="avatar">AI</div>
+              </div>
+            }
+            <div class="bubble-container">
+              <div class="message-bubble">
+                <div class="text" [innerHTML]="message.content"></div>
+                @if (message.role === 'assistant' && last) {
+                  <div class="message-actions">
+                    <button class="action-button" (click)="copyMessage(message.content)" title="Copy">⎘</button>
+                    <button class="action-button" title="Like">👍</button>
+                    <button class="action-button" title="Dislike">👎</button>
+                  </div>
+                }
+              </div>
+              <div class="timestamp">{{ message.timestamp | date:'h:mm a' }}</div>
+            </div>
+            @if (message.role === 'user') {
+              <div class="avatar-container">
+                <div class="avatar user">You</div>
+              </div>
+            }
+          </div>
+        </div>
+      </div>
+    }
+    
+    @if (isLoading() && messageList()[messageList().length - 1]?.role !== 'assistant') {
+      <div class="message-wrapper">
+        <div class="message assistant" [@messageAnimation]>
+          <div class="message-content">
+            <div class="avatar-container">
+              <div class="avatar">AI</div>
+            </div>
+            <div class="bubble-container">
+              <div class="message-bubble">
+                <div class="typing-indicator">
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    }
+  </div>
+
+  <!-- 输入区 -->
+  <div class="chat-input-container">
+    <div class="input-tools">
+      <button class="tool-button" (click)="openFilePicker()" title="Attach">📎</button>
+      <button class="tool-button" title="Voice Input">🎤</button>
+    </div>
+    <div class="input-wrapper" [class.focused]="inputFocused">
+      <textarea 
+        [(ngModel)]="userInput" 
+        (keydown)="handleKeyPress($event)"
+        (focus)="inputFocused = true"
+        (blur)="inputFocused = false"
+        placeholder="Ask me anything..."
+        [disabled]="isLoading()"
+        #messageInput
+      ></textarea>
+      <div class="input-actions">
+        <button 
+          class="send-button" 
+          (click)="sendMessage()" 
+          [disabled]="!userInput.trim() || isLoading()"
+          [class.loading]="isLoading()"
+          title="Send"
+        >
+          @if (isLoading()) {
+            <div class="spinner"></div>
+          } @else {
+            ↵
+          }
+        </button>
+      </div>
+    </div>
+  </div>
+
+  <!-- 历史会话侧边栏 -->
+  @if (showHistoryPanel) {
+    <div class="history-panel" [@panelAnimation]>
+      <div class="panel-header">
+        <h3>Conversation History</h3>
+        <button class="close-button" (click)="toggleHistory()" title="Close">✕</button>
+      </div>
+      <div class="history-list">
+        <div class="history-item" *ngFor="let item of historyItems">
+          <div class="item-content">{{ item.preview }}</div>
+          <div class="item-time">{{ item.time | date:'MMM d, h:mm a' }}</div>
+        </div>
+      </div>
+    </div>
+  }
+</div>

+ 563 - 0
industry-monitor-web/src/app/pages/ai-suggestions/page-text-response/page-text-response.scss

@@ -0,0 +1,563 @@
+/* 基础变量 */
+:root {
+  --primary-color: #6e48aa;
+  --primary-light: #9d50bb;
+  --secondary-color: #4776e6;
+  --text-primary: #2d3748;
+  --text-secondary: #718096;
+  --bg-color: #f8f9fa;
+  --bg-elevated: #ffffff;
+  --border-color: #e2e8f0;
+  --user-bubble: #edf2f7;
+  --ai-bubble: #ffffff;
+  --shadow-sm: 0 1px 3px rgba(0,0,0,0.12);
+  --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
+  --radius-md: 8px;
+  --radius-lg: 12px;
+}
+
+/* 动画定义 */
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(10px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes pulse {
+  0% { transform: scale(1); opacity: 1; }
+  50% { transform: scale(1.05); opacity: 0.7; }
+  100% { transform: scale(1); opacity: 1; }
+}
+
+@keyframes typing {
+  0% { opacity: 0.4; transform: translateY(0); }
+  50% { opacity: 1; transform: translateY(-3px); }
+  100% { opacity: 0.4; transform: translateY(0); }
+}
+
+/* 主容器 */
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: var(--bg-color);
+  position: relative;
+  overflow: hidden;
+}
+
+/* 顶部导航栏 */
+.chat-header {
+  background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
+  color: white;
+  padding: 0.75rem 1.5rem;
+  box-shadow: var(--shadow-md);
+  z-index: 10;
+
+  .header-content {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    max-width: 1200px;
+    margin: 0 auto;
+  }
+
+  .ai-identity {
+    display: flex;
+    align-items: center;
+    gap: 0.75rem;
+
+    h1 {
+      font-size: 1.25rem;
+      font-weight: 600;
+      margin: 0;
+    }
+  }
+
+  .avatar-pulse {
+    position: relative;
+    
+    .avatar {
+      width: 36px;
+      height: 36px;
+      border-radius: 50%;
+      background-color: white;
+      color: var(--primary-color);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-weight: bold;
+      z-index: 2;
+      position: relative;
+    }
+
+    .pulse-ring {
+      position: absolute;
+      top: -4px;
+      left: -4px;
+      right: -4px;
+      bottom: -4px;
+      background: rgba(255,255,255,0.3);
+      border-radius: 50%;
+      z-index: 1;
+      opacity: 0;
+      transition: opacity 0.3s ease;
+
+      &.active {
+        opacity: 1;
+        animation: pulse 2s infinite;
+      }
+    }
+  }
+
+  .status-indicator {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+    font-size: 0.875rem;
+    background: rgba(255,255,255,0.15);
+    padding: 0.25rem 0.75rem;
+    border-radius: 1rem;
+    transition: all 0.3s ease;
+
+    &.error {
+      background: rgba(239, 68, 68, 0.2);
+      color: #fef2f2;
+    }
+
+    &.loading {
+      .status-light {
+        animation: pulse 1.5s infinite;
+      }
+    }
+
+    .status-light {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: #4ade80;
+
+      .error & {
+        background: #ef4444;
+      }
+
+      .loading & {
+        background: #fbbf24;
+      }
+    }
+  }
+}
+
+/* 消息区域 */
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  scroll-behavior: smooth;
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+  max-width: 1200px;
+  margin: 0 auto;
+  width: 100%;
+  box-sizing: border-box;
+
+  .message-wrapper {
+    &.new-group {
+      margin-top: 1.25rem;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: -0.75rem;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 60%;
+        height: 1px;
+        background: var(--border-color);
+        opacity: 0.6;
+      }
+    }
+  }
+
+  .message {
+    max-width: 85%;
+    margin: 0 auto;
+    animation: fadeIn 0.3s ease-out forwards;
+    opacity: 0;
+
+    &.user {
+      margin-left: auto;
+      margin-right: 0;
+
+      .message-content {
+        flex-direction: row-reverse;
+      }
+
+      .message-bubble {
+        background-color: var(--user-bubble);
+        border-top-right-radius: 4px;
+      }
+    }
+
+    &.assistant {
+      margin-left: 0;
+      margin-right: auto;
+
+      .message-bubble {
+        background-color: var(--ai-bubble);
+        border-top-left-radius: 4px;
+        box-shadow: var(--shadow-sm);
+      }
+    }
+
+    .message-content {
+      display: flex;
+      gap: 0.75rem;
+      align-items: flex-end;
+    }
+
+    .avatar-container {
+      .avatar {
+        width: 32px;
+        height: 32px;
+        border-radius: 50%;
+        background: linear-gradient(135deg, var(--primary-light), var(--secondary-color));
+        color: white;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 0.75rem;
+        font-weight: bold;
+        flex-shrink: 0;
+
+        &.user {
+          background: var(--text-secondary);
+        }
+      }
+    }
+
+    .bubble-container {
+      display: flex;
+      flex-direction: column;
+      gap: 0.25rem;
+      flex: 1;
+      min-width: 0;
+    }
+
+    .message-bubble {
+      padding: 0.75rem 1rem;
+      border-radius: var(--radius-md);
+      line-height: 1.5;
+      word-wrap: break-word;
+      position: relative;
+      transition: transform 0.2s ease;
+
+      &:hover {
+        .message-actions {
+          opacity: 1;
+        }
+      }
+    }
+
+    .message-actions {
+      position: absolute;
+      right: 0;
+      top: -24px;
+      display: flex;
+      gap: 0.25rem;
+      background: white;
+      padding: 0.25rem;
+      border-radius: var(--radius-md);
+      box-shadow: var(--shadow-sm);
+      opacity: 0;
+      transition: opacity 0.2s ease;
+    }
+
+    .action-button {
+      width: 24px;
+      height: 24px;
+      border-radius: 4px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: none;
+      border: none;
+      color: var(--text-secondary);
+      cursor: pointer;
+      transition: all 0.2s ease;
+
+      &:hover {
+        background: var(--bg-color);
+        color: var(--primary-color);
+      }
+    }
+
+    .timestamp {
+      font-size: 0.75rem;
+      color: var(--text-secondary);
+      padding: 0 0.5rem;
+    }
+
+    .typing-indicator {
+      display: flex;
+      gap: 0.25rem;
+      padding: 0.5rem 0;
+
+      .dot {
+        width: 8px;
+        height: 8px;
+        border-radius: 50%;
+        background-color: var(--text-secondary);
+        opacity: 0.4;
+        animation: typing 1.4s infinite ease-in-out;
+
+        &:nth-child(1) { animation-delay: 0s; }
+        &:nth-child(2) { animation-delay: 0.2s; }
+        &:nth-child(3) { animation-delay: 0.4s; }
+      }
+    }
+  }
+}
+
+/* 输入区域 */
+.chat-input-container {
+  background: var(--bg-elevated);
+  border-top: 1px solid var(--border-color);
+  padding: 1rem;
+  position: relative;
+  max-width: 1200px;
+  margin: 0 auto;
+  width: 100%;
+  box-sizing: border-box;
+
+  .input-tools {
+    display: flex;
+    gap: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .tool-button {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: none;
+    border: none;
+    color: var(--text-secondary);
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background: var(--bg-color);
+      color: var(--primary-color);
+    }
+  }
+
+  .input-wrapper {
+    display: flex;
+    background: var(--bg-elevated);
+    border-radius: var(--radius-lg);
+    border: 1px solid var(--border-color);
+    transition: all 0.2s ease;
+    overflow: hidden;
+
+    &.focused {
+      border-color: var(--primary-color);
+      box-shadow: 0 0 0 1px var(--primary-color);
+    }
+
+    textarea {
+      flex: 1;
+      border: none;
+      padding: 0.75rem 1rem;
+      resize: none;
+      max-height: 150px;
+      min-height: 44px;
+      font-family: inherit;
+      line-height: 1.5;
+      background: transparent;
+      color: var(--text-primary);
+
+      &:focus {
+        outline: none;
+      }
+
+      &::placeholder {
+        color: var(--text-secondary);
+        opacity: 0.6;
+      }
+    }
+
+    .input-actions {
+      display: flex;
+      align-items: flex-end;
+      padding: 0 0.5rem 0.5rem;
+    }
+
+    .send-button {
+      width: 36px;
+      height: 36px;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: var(--primary-color);
+      border: none;
+      color: white;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      position: relative;
+      overflow: hidden;
+
+      &:disabled {
+        background: var(--border-color);
+        cursor: not-allowed;
+
+        svg {
+          opacity: 0.5;
+        }
+      }
+
+      &:not(:disabled):hover {
+        background: var(--primary-light);
+        transform: translateY(-2px);
+      }
+
+      &.loading {
+        background: var(--border-color);
+      }
+
+      svg {
+        transition: all 0.2s ease;
+
+        &.active {
+          color: white;
+        }
+      }
+
+      .spinner {
+        width: 20px;
+        height: 20px;
+        border: 2px solid rgba(255,255,255,0.3);
+        border-radius: 50%;
+        border-top-color: white;
+        animation: spin 1s ease-in-out infinite;
+      }
+    }
+  }
+}
+
+/* 历史面板 */
+.history-panel {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  width: 300px;
+  background: var(--bg-elevated);
+  border-left: 1px solid var(--border-color);
+  z-index: 20;
+  display: flex;
+  flex-direction: column;
+  box-shadow: -4px 0 12px rgba(0,0,0,0.05);
+  transform: translateX(100%);
+  animation: slideIn 0.3s ease-out forwards;
+
+  .panel-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 1rem;
+    border-bottom: 1px solid var(--border-color);
+
+    h3 {
+      margin: 0;
+      font-size: 1rem;
+      color: var(--text-primary);
+    }
+
+    .close-button {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: none;
+      border: none;
+      color: var(--text-secondary);
+      cursor: pointer;
+      transition: all 0.2s ease;
+
+      &:hover {
+        background: var(--bg-color);
+      }
+    }
+  }
+
+  .history-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 0.5rem;
+  }
+
+  .history-item {
+    padding: 0.75rem;
+    border-radius: var(--radius-md);
+    margin-bottom: 0.5rem;
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background: var(--bg-color);
+    }
+
+    .item-content {
+      font-size: 0.875rem;
+      color: var(--text-primary);
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      margin-bottom: 0.25rem;
+    }
+
+    .item-time {
+      font-size: 0.75rem;
+      color: var(--text-secondary);
+    }
+  }
+}
+
+/* 动画定义 */
+@keyframes slideIn {
+  from { transform: translateX(100%); }
+  to { transform: translateX(0); }
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .chat-messages {
+    padding: 1rem;
+
+    .message {
+      max-width: 90%;
+    }
+  }
+
+  .history-panel {
+    width: 280px;
+  }
+}
+.action-button {
+  font-size: 1.2em;
+  line-height: 1;
+}
+.send-button {
+  font-size: 1.5em;
+}

+ 23 - 0
industry-monitor-web/src/app/pages/ai-suggestions/page-text-response/page-text-response.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageTextResponse } from './page-text-response';
+
+describe('PageTextResponse', () => {
+  let component: PageTextResponse;
+  let fixture: ComponentFixture<PageTextResponse>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [PageTextResponse]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(PageTextResponse);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 173 - 0
industry-monitor-web/src/app/pages/ai-suggestions/page-text-response/page-text-response.ts

@@ -0,0 +1,173 @@
+import { Component, signal, effect, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TestCompletion } from '../../../../lib/completion';
+import { animate, style, transition, trigger } from '@angular/animations';
+
+interface ChatMessage {
+  role: 'user' | 'assistant';
+  content: string;
+  timestamp?: Date;
+}
+
+interface HistoryItem {
+  id: string;
+  preview: string;
+  time: Date;
+  messages: ChatMessage[];
+}
+
+@Component({
+  selector: 'app-chat',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './page-text-response.html',
+  styleUrls: ['./page-text-response.scss'],
+  animations: [
+    trigger('messageAnimation', [
+      transition(':enter', [
+        style({ opacity: 0, transform: 'translateY(10px)' }),
+        animate('200ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
+      ])
+    ]),
+    trigger('panelAnimation', [
+      transition(':enter', [
+        style({ transform: 'translateX(100%)' }),
+        animate('250ms ease-out', style({ transform: 'translateX(0)' }))
+      ]),
+      transition(':leave', [
+        animate('200ms ease-in', style({ transform: 'translateX(100%)' }))
+      ])
+    ])
+  ]
+})
+export class PageTextResponse implements AfterViewChecked {
+  userInput = '';
+  isLoading = signal(false);
+  errorMessage = signal<string | null>(null);
+  messageList = signal<ChatMessage[]>([]);
+  showHistoryPanel = false;
+  inputFocused = false;
+  historyItems: HistoryItem[] = [];
+  
+  @ViewChild('messagesContainer') private messagesContainer!: ElementRef;
+  @ViewChild('messageInput') private messageInput!: ElementRef;
+
+  constructor() {
+    // 模拟历史数据
+    this.historyItems = Array.from({ length: 5 }, (_, i) => ({
+      id: `hist-${i}`,
+      preview: `Test conversation ${i + 1} about AI assistants...`,
+      time: new Date(Date.now() - (i * 24 * 60 * 60 * 1000)),
+      messages: []
+    }));
+
+    // 添加欢迎消息
+    effect(() => {
+      if (this.messageList().length === 0) {
+        setTimeout(() => {
+          this.addWelcomeMessage();
+        }, 500); // 延迟显示以获得更好的加载体验
+      }
+    });
+  }
+
+  ngAfterViewChecked() {
+    this.scrollToBottom();
+  }
+
+  private scrollToBottom(): void {
+    try {
+      this.messagesContainer.nativeElement.scrollTop = this.messagesContainer.nativeElement.scrollHeight;
+    } catch(err) { }
+  }
+
+  private addWelcomeMessage() {
+    this.messageList.update(messages => [
+      ...messages,
+      {
+        role: 'assistant',
+        content: 'Hello! I\'m your AI assistant. How can I help you today?',
+        timestamp: new Date()
+      }
+    ]);
+  }
+
+  isNewGroup(index: number): boolean {
+    if (index === 0) return false;
+    const prevMsg = this.messageList()[index - 1];
+    const currentMsg = this.messageList()[index];
+    
+    // 如果超过5分钟或角色变化,则视为新组
+    const timeDiff = (currentMsg.timestamp!.getTime() - prevMsg.timestamp!.getTime()) / (1000 * 60);
+    return timeDiff > 5 || prevMsg.role !== currentMsg.role;
+  }
+
+  toggleHistory() {
+    this.showHistoryPanel = !this.showHistoryPanel;
+  }
+
+  async sendMessage() {
+    if (!this.userInput.trim()) return;
+
+    const userMessage: ChatMessage = {
+      role: 'user',
+      content: this.userInput,
+      timestamp: new Date()
+    };
+
+    // 添加用户消息
+    this.messageList.update(messages => [...messages, userMessage]);
+    this.userInput = '';
+    this.isLoading.set(true);
+    this.errorMessage.set(null);
+
+    // 聚焦输入框以保持键盘打开(移动端)
+    setTimeout(() => {
+      this.messageInput.nativeElement.focus();
+    }, 100);
+
+    try {
+      const completion = new TestCompletion(this.messageList());
+      
+      let assistantMessageIndex = this.messageList().length;
+      this.messageList.update(messages => [...messages, { 
+        role: 'assistant', 
+        content: '', 
+        timestamp: new Date() 
+      }]);
+
+      await completion.sendMessage(null, (content) => {
+        this.messageList.update(messages => {
+          const updated = [...messages];
+          updated[assistantMessageIndex].content = content;
+          return updated;
+        });
+      });
+    } catch (error) {
+      console.error('Error sending message:', error);
+      this.errorMessage.set('Failed to get response. Please try again.');
+    } finally {
+      this.isLoading.set(false);
+    }
+  }
+
+  handleKeyPress(event: KeyboardEvent) {
+    if (event.key === 'Enter' && !event.shiftKey) {
+      event.preventDefault();
+      this.sendMessage();
+    }
+  }
+
+  copyMessage(content: string) {
+    navigator.clipboard.writeText(content).then(() => {
+      // 可以在这里添加复制成功的反馈
+      console.log('Message copied to clipboard');
+    });
+  }
+
+  openFilePicker() {
+    // 实现文件选择逻辑
+    console.log('Open file picker');
+  }
+}

+ 92 - 0
industry-monitor-web/src/lib/completion.ts

@@ -0,0 +1,92 @@
+
+export interface TestMessage{
+    role:string
+    content:string
+}
+
+export class TestCompletion{
+    token:string = "r:60abef69e7cd8181b146ceaba1fdbf02"
+    messageList:any = []
+    stream:boolean = true;
+    constructor(messageList:any){
+        this.messageList = messageList || this.messageList
+    }
+    async sendMessage(messageList?:null|Array<TestMessage>,onMessage?: (content: string) => void):Promise<any>{
+        
+        this.messageList = messageList || this.messageList
+        let body = {
+            "messages": this.messageList,
+            "stream": this.stream,
+            "model": "fmode-4.5-128k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "token": "Bearer "+this.token
+        }
+
+        let response = await fetch("https://server.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+            "headers": {
+            },
+            "body": JSON.stringify(body),
+            "method": "POST",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+
+        
+        /** 单次响应 HTTP短连接请求
+         {"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"您好!我是一个人工智能助手,旨在帮助您回答问题、提供信息和解决各种问题。我可以处理许多主题,包括科技、历史、文化、语言学习等。如果您有任何具体的问题或需要了解的内容,请随时告诉我!","refusal":null,"role":"assistant"}}],"created":1751509370,"id":"chatcmpl-Bp3t41MP4pb2NR38n1ylrJw922SBZ","model":"gpt-4o-mini-2024-07-18","object":"chat.completion","system_fingerprint":"fp_efad92c60b","usage":{"completion_tokens":55,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":15,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":70}}
+        */
+        if(this.stream == false){
+            let data = await response.json()
+            console.log(data)
+            let lastContent = data?.choices?.[0]?.message?.content
+            return lastContent
+        }
+        /**
+         * 流式加载 HTTP Event Stream 模式 长连接获取
+         */
+        
+        // Stream mode handling
+        if (!response.body) {
+            throw new Error("No response body in stream mode");
+        }
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder("utf-8");
+        let accumulatedContent = "";
+        try {
+            while (true) {
+                const { done, value } = await reader.read();
+                if (done) break;
+
+                const chunk = decoder.decode(value, { stream: true });
+                const lines = chunk.split('\n').filter(line => line.trim() !== '');
+
+                for (const line of lines) {
+                    if (line.startsWith('data:') && !line.includes('[DONE]')) {
+                        try {
+                            console.log("line",line)
+                            const jsonStr = line.substring(5).trim();
+                            const data = JSON.parse(jsonStr);
+                            const content = data?.choices?.[0]?.delta?.content || '';
+                            
+                            if (content) {
+                                accumulatedContent += content;
+                                if (onMessage) {
+                                    onMessage(accumulatedContent);
+                                }
+                            }
+                        } catch (e) {
+                            console.error("Error parsing stream data:", e);
+                        }
+                    }
+                }
+            }
+        } finally {
+            reader.releaseLock();
+        }
+
+        return accumulatedContent;
+    }
+}