Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6abf44c
Chatbot appears bottom left and persists across steps. Does not save …
hirokiterashima Dec 24, 2025
3d527f6
Let user create new chats
hirokiterashima Dec 24, 2025
4175811
Clean up code, make more readable
hirokiterashima Dec 24, 2025
216d40e
Chatbot create and retrieve chats working with dummy backend
hirokiterashima Dec 24, 2025
f7db8ef
Let user edit chat title
hirokiterashima Dec 29, 2025
8e53b77
Let user soft delete chats
hirokiterashima Dec 29, 2025
3b5058d
Remove unused code
hirokiterashima Dec 29, 2025
e11ac8e
Add nodeId field to ChatMessage
hirokiterashima Dec 30, 2025
2148e47
refactor: move runId and workgroupId to class variable
hirokiterashima Dec 30, 2025
b0878a4
refactor: create ChatMessage class
hirokiterashima Dec 30, 2025
4b49290
refactor: move chat http post to ChatService.
hirokiterashima Dec 30, 2025
9205cf0
refactor: move domain classes into separate file
hirokiterashima Dec 30, 2025
382c71d
Add AWSBedRockService option
hirokiterashima Jan 5, 2026
5c8d28d
Ignore line endings in reasoning block from chat endpoint
hirokiterashima Jan 5, 2026
eef8b27
Author system prompt in chatbot settings. Ensure that nodeId is never…
hirokiterashima Jan 5, 2026
112a114
Updated messages
github-actions[bot] Jan 5, 2026
8c30b6a
Refactor common code in ChatService and AWSBedrockService
hirokiterashima Jan 6, 2026
811e926
Show last edited chat when user comes back
hirokiterashima Jan 6, 2026
24b369b
Updated messages
github-actions[bot] Jan 6, 2026
8f51303
Parse and display markdown in responses from chatbot
hirokiterashima Jan 7, 2026
1839853
Add tests for ChatbotComponent
hirokiterashima Jan 7, 2026
c8e22a5
Ensure that links in chat responses in open in a new tab
hirokiterashima Jan 7, 2026
b24c59d
Updated chatbot styles and layout
breity Jan 7, 2026
8283a93
Updated messages
github-actions[bot] Jan 7, 2026
228c6b4
Move chat list from MatMenu to MatDialog. Editing chat title, selecti…
hirokiterashima Jan 9, 2026
fec0f94
If no current chat, open last edited when closing chat history
breity Jan 12, 2026
c7d55a9
Update styles for chat history dialog
breity Jan 12, 2026
dadf6b4
Updated messages
github-actions[bot] Jan 12, 2026
a9f276c
Generate and set chat title with AI based on user's first message
hirokiterashima Jan 13, 2026
ee979e2
Move chatbot to sidebar; move notes launcher to step toolbar
breity Jan 14, 2026
f238216
Show instructions when chat has no messages; fix test
breity Jan 14, 2026
57766c0
Updated messages
github-actions[bot] Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,566 changes: 1,501 additions & 65 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@
"jquery": "^3.6.0",
"jwt-decode": "^3.1.2",
"lz-string": "^1.4.4",
"marked": "^16.4.2",
"mathjax": "^3.2.2",
"microphone-stream": "^6.0.1",
"ng-file-upload": "^12.2.13",
"ng-recaptcha-2": "^16.0.1",
"ngx-markdown": "^20.1.0",
"process": "^0.11.10",
"rxjs": "^7.5.6",
"sockjs-client": "^1.6.0",
Expand Down
12 changes: 12 additions & 0 deletions src/app/chatbot/awsBedRock.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Injectable } from '@angular/core';
import { ChatService } from './chat.service';

@Injectable({ providedIn: 'root' })
export class AwsBedRockService extends ChatService {
protected chatEndpoint = '/api/aws-bedrock/chat';
protected model: string = 'openai.gpt-oss-20b-1:0';

processResponse(response: string): string {
return response.replace(/<reasoning>.*?<\/reasoning>/gs, '');
}
}
89 changes: 89 additions & 0 deletions src/app/chatbot/chat-history-dialog.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<h2 matDialogTitle i18n>Conversation History</h2>
<mat-dialog-content class="!p-0">
<mat-action-list class="!p-0">
<mat-divider />
@for (chat of data.chats; track chat.id) {
@if (editingChatId === chat.id) {
<button mat-list-item>
<div matListItemLine class="!flex !flex-grow items-center h-full">
<mat-form-field class="flex-grow" subscriptSizing="dynamic">
<input
matInput
[(ngModel)]="editingTitle"
(keypress)="handleEditKeyPress($event, chat)"
[placeholder]="$any('Chat title')"
i18n-placeholder
autofocus
/>
</mat-form-field>
</div>
<span matListItemLine></span>
<div matListItemMeta>
<button
matIconButton
(click)="saveEdit(chat)"
[disabled]="!editingTitle.trim()"
matTooltip="Save"
i18n-matTooltip
matTooltipPosition="above"
>
<mat-icon>check</mat-icon>
</button>
<button
matIconButton
(click)="cancelEdit()"
matTooltip="Cancel"
i18n-matTooltip
matTooltipPosition="above"
>
<mat-icon>close</mat-icon>
</button>
</div>
</button>
} @else {
<button mat-list-item (click)="switchToChat(chat)">
<span matListItemTitle
>{{ chat.title }}
@if (data.currentChatId === chat.id) {
<span class="mat-caption" i18n>(current)</span>
}
</span>
<span class="mat-caption"
>{{ formatDate(chat.lastUpdated) }}, {{ getMessageCount(chat) }}
@if (getMessageCount(chat) === 1) {
<span i18n>message</span>
} @else {
<span i18n>messages</span>
}
</span>
<div matListItemMeta>
<button
matIconButton
(click)="$event.stopPropagation(); startEdit(chat)"
matTooltip="Edit title"
matTooltipPosition="above"
i18n-matTooltip
>
<mat-icon>edit</mat-icon>
</button>
<button
matIconButton
(click)="$event.stopPropagation(); deleteChat(chat)"
matTooltip="Delete chat"
matTooltipPosition="above"
i18n-matTooltip
>
<mat-icon>delete</mat-icon>
</button>
</div>
</button>
}
<mat-divider />
}
</mat-action-list>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">
<span i18n>Close</span>
</button>
</mat-dialog-actions>
7 changes: 7 additions & 0 deletions src/app/chatbot/chat-history-dialog.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.mdc-list-item--with-trailing-meta.mdc-list-item--with-two-lines .mdc-list-item__end {
align-self: center;
}

.mat-divider {
margin: 0;
}
127 changes: 127 additions & 0 deletions src/app/chatbot/chat-history-dialog.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Chat } from './chat';
import { ChatbotService } from './chatbot.service';

export interface ChatHistoryDialogData {
chats: Chat[];
currentChatId: string | null;
runId: number;
workgroupId: number;
}

@Component({
selector: 'chat-history-dialog',
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
MatIconModule,
MatListModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatTooltipModule
],
templateUrl: 'chat-history-dialog.component.html',
styleUrl: 'chat-history-dialog.component.scss'
})
export class ChatHistoryDialogComponent {
protected dialogRef = inject(MatDialogRef<ChatHistoryDialogComponent>);
private chatbotService: ChatbotService = inject(ChatbotService);
protected data: ChatHistoryDialogData = inject(MAT_DIALOG_DATA);
protected editingChatId: string | null = null;
protected editingTitle: string = '';

protected startEdit(chat: Chat): void {
this.editingChatId = chat.id;
this.editingTitle = chat.title;
}

protected cancelEdit(): void {
this.editingChatId = null;
this.editingTitle = '';
}

protected async saveEdit(chat: Chat): Promise<void> {
if (!this.editingTitle.trim()) {
return;
}
chat.title = this.editingTitle.trim();
await this.chatbotService.updateChat(this.data.runId, this.data.workgroupId, chat);
this.cancelEdit();
}

protected handleEditKeyPress(event: KeyboardEvent, chat: Chat): void {
if (event.key === 'Enter') {
event.preventDefault();
this.saveEdit(chat);
} else if (event.key === 'Escape') {
event.preventDefault();
this.cancelEdit();
}
}

protected async deleteChat(chat: Chat): Promise<void> {
const msg = $localize`Are you sure you want to delete "${chat.title}"? This action cannot be undone.`;
if (confirm(msg)) {
await this.chatbotService.deleteChat(this.data.runId, this.data.workgroupId, chat.id);
const chatIndex = this.data.chats.findIndex((c) => c.id === chat.id);
this.data.chats.splice(chatIndex, 1);
if (this.data.currentChatId === chat.id) {
if (this.data.chats.length > 0) {
this.data.currentChatId = this.data.chats[0].id;
} else {
this.data.currentChatId = null;
}
}
}
}

protected close(): void {
this.switchToChat(this.data.chats.find((c) => c.id === this.data.currentChatId));
}

protected switchToChat(chat: Chat): void {
if (this.editingChatId) {
return; // don't switch if we're editing
}
this.dialogRef.close(chat);
}

protected formatDate(date: Date): string {
const now = new Date();
const chatDate = new Date(date);
const diffMs = now.getTime() - chatDate.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);

if (diffMins < 1) {
return $localize`Just now`;
} else if (diffMins < 60) {
return $localize`${diffMins}m ago`;
} else if (diffHours < 24) {
return $localize`${diffHours}h ago`;
} else if (diffDays < 7) {
return $localize`${diffDays}d ago`;
} else {
return chatDate.toLocaleDateString();
}
}

protected getMessageCount(chat: Chat): number {
// Don't count system messages
return chat.messages.filter((msg) => msg.role !== 'system').length;
}
}
55 changes: 55 additions & 0 deletions src/app/chatbot/chat.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { inject, Injectable } from '@angular/core';
import { ChatMessage } from './chat';
import { firstValueFrom } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class ChatService {
protected chatEndpoint = '/api/chat-gpt';
private http = inject(HttpClient);
protected model: string = 'gpt-3.5-turbo';

/**
* Sends a message to the chat-gpt endpoint.
* @param messages The conversation history.
* @returns A promise that resolves to the response from the chat-gpt endpoint.
*/
async sendMessage(messages: ChatMessage[]): Promise<string> {
const payload = {
messages: messages.map((msg) => ({
role: msg.role,
content: msg.content
})),
model: this.model
};
try {
const response = await firstValueFrom(this.http.post<any>(`${this.chatEndpoint}`, payload));
return this.processResponse(response.choices[0].message.content);
} catch (error) {
console.error('Error calling chat endpoint:', error);
throw error;
}
}

processResponse(response: string): string {
return response;
}

/**
* Generates a short, concise title for a chat based on the first message.
* @param message The first user message content.
* @returns A promise that resolves to the generated title.
*/
async generateChatTitle(message: string): Promise<string> {
const prompt = `Generate a short, concise title (max 5 words) for a chat that starts with this message: "${message}". Respond only with the title, no quotes or extra text. If the language of the message is not English, return the title in that language.`;
const messages: ChatMessage[] = [
new ChatMessage(
'system',
'You are a helpful assistant that generates short titles for chat conversations.',
''
),
new ChatMessage('user', prompt, '')
];
return this.sendMessage(messages);
}
}
21 changes: 21 additions & 0 deletions src/app/chatbot/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface Chat {
id: string;
title: string;
messages: ChatMessage[];
createdAt: Date;
lastUpdated: Date;
deleted: boolean;
}

export class ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
nodeId: string;
timestamp: Date = new Date();

constructor(role: 'user' | 'assistant' | 'system', content: string, nodeId: string) {
this.role = role;
this.content = content;
this.nodeId = nodeId;
}
}
Loading