-
-
-
diff --git a/src/kc_angular/src/app/components/chat-components/chat.view.component.scss b/src/kc_angular/src/app/components/chat-components/chat.view.component.scss
deleted file mode 100755
index 0b1a13ce..00000000
--- a/src/kc_angular/src/app/components/chat-components/chat.view.component.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-/*!
- * Copyright (c) 2023 Rob Royce
- *
- * Licensed 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.
- */
-
-.user-message {
- background-color: var(--primary-color) !important;
- color: var(--primary-color-text) !important;
- border-radius: 10px;
- padding: 0 10px 0 10px;
- margin: 10px;
- text-align: right;
- max-width: 80%;
-}
-
-.agent-message {
- color: var(--primary-color) !important;
- background-color: var(--primary-color-text) !important;
- border-radius: 10px;
- padding: 0 10px 0 10px;
- margin: 10px;
- text-align: left;
- max-width: 80%;
-}
-
-#chat-toolbar {
- height: 2rem;
- background-color: var(--surface-ground);
-}
-
-.next-question-suggestion:hover {
- opacity: 1 !important;
-}
-
-.html2canvas-container {
- width: 3000px !important;
- height: 3000px !important;
-}
diff --git a/src/kc_angular/src/app/components/chat-components/chat.view.component.ts b/src/kc_angular/src/app/components/chat-components/chat.view.component.ts
index 05dd68d7..5d556b1b 100755
--- a/src/kc_angular/src/app/components/chat-components/chat.view.component.ts
+++ b/src/kc_angular/src/app/components/chat-components/chat.view.component.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 Rob Royce
+ * Copyright (c) 2023-2024 Rob Royce
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,173 +14,117 @@
* limitations under the License.
*/
-import {
- Component,
- ElementRef,
- EventEmitter,
- Input,
- OnChanges,
- OnInit,
- Output,
- SimpleChanges,
- ViewChild,
-} from '@angular/core';
+import { Component, Input, ViewChild } from '@angular/core';
import { ChatService } from '@app/services/chat-services/chat.service';
-import { debounceTime, finalize, map, take, tap } from 'rxjs/operators';
-import { ConfirmationService, MenuItem, PrimeIcons } from 'primeng/api';
+import { MenuItem } from 'primeng/api';
import { ContextMenu } from 'primeng/contextmenu';
-import { DialogService } from 'primeng/dynamicdialog';
-import { Clipboard } from '@angular/cdk/clipboard';
-import { MarkdownService } from '@app/services/factory-services/markdown.service';
-import {
- KcNotification,
- NotificationsService,
-} from '@app/services/user-services/notifications.service';
-import { ChatContextMenuService } from '@app/services/factory-services/chat-context-menu.service';
import { AgentType, ChatMessage } from '@app/models/chat.model';
-import { ChatPrompts } from '@services/chat-services/prompts';
-import { SettingsService } from '@services/ipc-services/settings.service';
-import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
+import { Observable } from 'rxjs';
+import { ChatContextMenuService } from '@services/factory-services/chat-context-menu.service';
+import { ChatCommandService } from '@services/chat-services/commands.service';
+import { distinctUntilChanged, tap } from 'rxjs/operators';
@Component({
selector: 'app-chat-view',
- templateUrl: './chat.view.component.html',
- styleUrls: ['./chat.view.component.scss'],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+ No Messages
+
+
+
+
+
+
+
+
+
+ `,
+ styles: [
+ `
+ #chat-toolbar {
+ height: 2rem;
+ background-color: var(--surface-ground);
+ }
+ `,
+ ],
})
-export class ChatViewComponent implements OnInit, OnChanges {
- /* The input element for the chat */
- @ViewChild('chatInput', { static: true }) chatInput!: ElementRef;
-
+export class ChatViewComponent {
/* The context menu for chat messages */
@ViewChild('cm', { static: true }) cm!: ContextMenu;
- /* The chat history to display */
- @Input() history: ChatMessage[] = [];
-
- /* Whether the chat is loading (e.g. waiting for a response from the server) */
- @Input() loading = false;
-
/* Whether to apply a height restriction to the chat history */
@Input() heightRestricted = false;
- /* The event emitted when the user submits a message */
- @Output() submit: EventEmitter = new EventEmitter();
-
- /* The event emitted when the user clicks on the Regenerate button */
- @Output() regenerate: EventEmitter =
- new EventEmitter();
-
- /* The event emitted when the user clicks on the Delete button */
- @Output() delete: EventEmitter = new EventEmitter<
- ChatMessage[]
- >();
-
- /* An array of suggestions for the next question */
- nextQuestionSuggestions: string[] = [];
-
- /* The context menu used to display the options for a message */
messageMenu: MenuItem[] = [];
- suggestions = true;
-
- focusTrap = true;
-
- editedMessage?: ChatMessage;
-
- filtered$: Observable;
-
- private _history$ = new BehaviorSubject([]);
-
- history$ = this._history$.asObservable();
-
- private filter = new BehaviorSubject('');
-
- filter$ = this.filter.asObservable();
-
- private messageInput = new BehaviorSubject('');
+ messages$: Observable;
constructor(
- private clipboard: Clipboard,
private chat: ChatService,
private context: ChatContextMenuService,
- private confirm: ConfirmationService,
- private dialog: DialogService,
- private markdown: MarkdownService,
- private notify: NotificationsService,
- private prompts: ChatPrompts,
- private settings: SettingsService
+ private command: ChatCommandService
) {
- settings.all
- .pipe(
- map((settings) => settings.app.chat.suggestions),
- debounceTime(250),
- tap((suggestions) => {
- this.suggestions = suggestions.enabled;
- this.focusTrap = suggestions.onInput;
- })
- )
- .subscribe();
-
- this.history$.pipe(debounceTime(100)).subscribe((history) => {
- if (history.length === 0) {
- // Add an introductory message to the chat history
- const msg = this.chat.createMessage(
- AgentType.Knowledge,
- AgentType.User,
- "Welcome to Knowledge Chat! I'm here to help you with any questions or research needs you may have. Whether you need help with a project or just want to learn more about a topic, I'm here to assist you. Please feel free to ask me anything!"
- );
- this._history$.next([msg]);
- }
- });
-
- // Filter the chat history based on the filter text
- this.filtered$ = combineLatest([this.filter$, this.history$]).pipe(
- debounceTime(100),
- map(([filter, history]) => {
- if (!filter) return history;
-
- return history.filter((message) => {
- return message.text.toLowerCase().includes(filter.toLowerCase());
- });
+ this.messages$ = this.chat.messages$.pipe(
+ distinctUntilChanged((a, b) => {
+ // If the messages are the same length, check if they are the same messages
+ if (a.length === b.length) {
+ for (let i = 0; i < a.length; i++) {
+ if (a[i].id !== b[i].id) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }),
+ tap(() => {
+ this.scroll();
})
);
- this.messageInput
- .asObservable()
- .pipe(debounceTime(1000))
- .subscribe((message) => {
- this.chat.countTokens(message);
- });
- }
-
- ngOnInit(): void {
- // Scroll to the bottom of the chat history after rest of component finishes loading
- setTimeout(() => {
+ this.command.scrollView$.asObservable().subscribe(() => {
this.scroll();
- }, 50);
- }
-
- /**
- * If the history changes, reset the suggestions and sort the history by timestamp
- * If the suggestions are enabled, use the most recent message to generate suggestions
- */
- ngOnChanges(changes: SimpleChanges) {
- if (changes.history) {
- // Reset the suggestions when the history changes
- this.nextQuestionSuggestions = [];
- this.filter.next('');
- this._history$.next(changes.history.currentValue);
-
- setTimeout(() => {
- if (
- changes.history?.currentValue?.length ===
- changes.history?.previousValue?.length - 1
- ) {
- return;
- }
- this.scroll();
- }, 1000);
- }
+ });
}
/* Scroll to the end of the #chat-history element */
@@ -195,227 +139,19 @@ export class ChatViewComponent implements OnInit, OnChanges {
}
}
- sendMessage(message: string) {
- if (!message) {
- return;
- }
-
- this.submit.emit(message);
- this.chat.countTokens('');
- this.scroll();
- }
-
- /**
- * Generate the next question suggestions for the given message
- *
- * @param message The message to generate suggestions for
- * @description The suggestions are generated using the message and the 14 messages prior to it
- */
- generateNextQuestion(message: ChatMessage) {
- if (!this.settings.get().app.chat.suggestions.enabled) {
- return;
- }
-
- // Get the message and the 14 messages prior to it, then send those to the prompt service
- this.chat.getHistory(message, this.history, true);
- const index = this.history.indexOf(message);
- const messages = this.history.slice(index - 14, index + 1);
-
- this.prompts
- .predictNextQuestion(messages, message.source ? 'source' : 'project')
- .pipe(
- take(1),
- tap((response) => {
- this.nextQuestionSuggestions = response;
- this.scroll();
- })
- )
- .subscribe();
- }
-
- chatFocusTrap() {
- if (this.suggestions && this.focusTrap) {
- this.generateNextQuestion(this.history[this.history.length - 1]);
- }
- }
-
- /* Submit the given suggestion as the next question */
- sendSuggestion(suggestion: string) {
- if (this.loading) {
- return;
- }
- this.submit.emit(suggestion);
- this.nextQuestionSuggestions = this.nextQuestionSuggestions.filter(
- (s) => s !== suggestion
- );
-
- if (this.nextQuestionSuggestions.length === 0) {
- this.generateNextQuestion(this.history[this.history.length - 1]);
- }
- }
-
- /**
- * Regenerate the given message, replacing it with a new message
- * @param message The message to regenerate
- * @param warn Whether to warn the user that the message will be replaced in the chat history
- * @description This will replace the message in the chat history with a new message
- */
- regenerateMessage(message: ChatMessage, warn = true) {
- if (warn) {
- this.warnUser(
- [message],
- `Regenerate`,
- `Are you sure you want to regenerate this message? This will replace the message in the chat history.`,
- `Regenerate Message`,
- this.regenerate
- );
+ blurHistory($event: boolean) {
+ // If the command bar is active, make the chat history slightly opaque and transparent
+ if ($event) {
+ document
+ .getElementsByClassName('chat-history')[0]
+ .classList.add('chat-history-active');
} else {
- this.regenerate.emit(message);
+ document
+ .getElementsByClassName('chat-history')[0]
+ .classList.remove('chat-history-active');
}
}
- /**
- * Delete the given message from the chat history and storage
- * @param message The message to delete
- * @description This will delete the message from the chat history and storage
- */
- deleteMessage(message: ChatMessage) {
- this.warnUser(
- [message],
- `Delete`,
- `Are you sure you want to permanently delete this message?`,
- `Delete Message`,
- this.delete
- );
- }
-
- /**
- * Edit the given message in the chat history
- * @param message The message to edit
- * @param edited (optional) The edited message to replace the original message with
- * @description This will replace the message in the chat history with the edited message.
- * If no edited message is given, the message will turn into an input field. If an edited message is given,
- * the message will be replaced with the edited message and the response will be regenerated.
- */
- editMessage(message: ChatMessage, edited?: string) {
- if (edited && edited !== message.text) {
- message.text = edited;
-
- // If this is a user message, regenerate the response to the message
- if (message.sender === AgentType.User) {
- // Get the message that was a response to the message being edited and regenerate it
- const index = this.history.indexOf(message);
- if (index + 1 <= this.history.length) {
- const response = this.history[index + 1];
-
- if (response.sender === message.recipient) {
- // If the next message is from the same agent, regenerate the message
- this.regenerateMessage(response, false);
- } else {
- // Otherwise, we need to insert a new response into the history immediately after the edited message
- const newResponse = this.chat.createMessage(
- message.recipient,
- AgentType.User,
- '',
- message.project,
- message.source
- );
- this.history.splice(index + 1, 0, newResponse);
- this.regenerateMessage(newResponse, false);
- }
- }
- }
- }
- this.editedMessage = edited ? undefined : message;
- }
-
- /**
- * Summarize the given message using the chat service
- * @param message The message to summarize
- * @description This will add a tldr section to the top of the message
- */
- tldr(message: ChatMessage) {
- // Get most recent messages leading up to the message to summarize
- const index = this.history.indexOf(message);
- const messages = this.history.slice(index - 14, index + 1);
-
- this.loading = true;
- this.chat
- .elaborate(messages, 'TLDR')
- .pipe(
- take(1),
- tap((response) => {
- message.text += `
-
-**TLDR:** ${response.content}`;
- this.chat.saveChat(this.history);
- }),
- finalize(() => {
- this.loading = false;
- })
- )
- .subscribe();
- }
-
- /**
- * Explain the given message to a 5-year-old
- * @param message The message to explain
- * @description This will append the ELI5 explaination to the bottom of the message
- */
- explainLikeIm5(message: ChatMessage) {
- // Get most recent messages leading up to the message to explain
- this.loading = true;
- const index = this.history.indexOf(message);
- const messages = this.history.slice(index - 14, index + 1);
- this.chat
- .elaborate(messages, 'ELI5')
- .pipe(
- take(1),
- tap((response) => {
- message.text += `
-
-**ELI5:** ${response.content}`;
- this.chat.saveChat(this.history);
- }),
- finalize(() => {
- this.loading = false;
- })
- )
- .subscribe();
- }
-
- /**
- * Continue the response to the given message (useful when the response is too long to fit in one message)
- * @param message
- */
- continueResponse(message: ChatMessage) {
- // Get most recent messages leading up to the message to explain
- this.loading = true;
- const index = this.history.indexOf(message);
- const messages = this.history.slice(index - 14, index + 1);
- this.chat
- .elaborate(messages, 'Continue')
- .pipe(
- take(1),
- tap((response) => {
- message.text += `${response.content}`;
- this.chat.saveChat(this.history);
- }),
- finalize(() => {
- this.loading = false;
- })
- )
- .subscribe();
- }
-
- /**
- * Filter the chat history by the given string
- * @param $event The string to filter by
- */
- onFilter($event: string) {
- this.filter.next($event);
- }
-
/**
* Show the context menu for the given message at the given event location
* @param $event The event to show the context menu at
@@ -431,24 +167,18 @@ export class ChatViewComponent implements OnInit, OnChanges {
const text = selection.toString().trim();
const tokens = text.split(' ');
if (text.length > 0 && tokens.length > 0 && tokens.length <= 5) {
- this.messageMenu.push(
- this.context.topics(message, text, () => {
- this.chat.saveChat(this.history);
- })
- );
+ this.messageMenu.push(this.context.topics(message, text, () => {}));
}
+
// Add the search option
- this.messageMenu.push(
- this.context.search(message, selection, () => {
- this.chat.saveChat(this.history);
- })
- );
+ this.messageMenu.push(this.context.search(message, selection, () => {}));
}
// If the user has highlighted text, add a highlight option
const highlight = this.context.highlight(message, $event, () => {
- this.chat.saveChat(this.history);
+ this.chat.persistChatHistory();
});
+
if (highlight) {
this.messageMenu.push(highlight);
}
@@ -466,120 +196,17 @@ export class ChatViewComponent implements OnInit, OnChanges {
this.messageMenu.push(this.context.save(message));
}
- this.messageMenu.push(this.context.copy(message));
- this.messageMenu.push(
- this.context.edit(() => {
- this.editMessage(message);
- })
- );
-
- // If the message is not a user message, add continue, regenerate, tldr, and eli5 options
- if (message.sender !== AgentType.User) {
+ if (this.messageMenu.length > 0) {
this.messageMenu.push(this.context.separator());
- this.messageMenu.push(
- this.context.continue(() => {
- this.continueResponse(message);
- })
- );
- this.messageMenu.push(
- this.context.regenerate(() => {
- this.regenerateMessage(message);
- })
- );
- this.messageMenu.push(
- this.context.tldr(() => {
- this.tldr(message);
- })
- );
- this.messageMenu.push(
- this.context.eli5(() => {
- this.explainLikeIm5(message);
- })
- );
}
- this.messageMenu.push(this.context.separator());
this.messageMenu.push(
+ this.context.copy(message),
this.context.delete(() => {
- this.deleteMessage(message);
+ this.chat.deleteMessage(message);
})
);
this.cm.show($event);
}
-
- rating(rating: 'thumbs-up' | 'thumbs-down' | 'none', message: ChatMessage) {
- message.rating = rating;
-
- this.notify.debug('Chat View', rating.toLocaleUpperCase(), message);
- this.chat.saveChat(this._history$.value);
-
- if (rating === 'none') {
- return;
- }
-
- const msg: KcNotification = {
- severity: rating === 'thumbs-up' ? 'success' : 'warn',
- icon:
- rating === 'thumbs-up' ? PrimeIcons.THUMBS_UP : PrimeIcons.THUMBS_DOWN,
- summary: rating === 'thumbs-up' ? 'Context++' : 'Context--',
- detail:
- rating === 'thumbs-up'
- ? `This message will be prioritized for providing context.`
- : `This message will not be used to provide context.`,
- closable: true,
- presentation: 'toast',
- };
- this.notify.broadcast(msg);
- }
-
- /**
- * Warn the user that the given action will replace the message in the chat history
- * @param messages The message(s) to regenerate or delete
- * @param acceptLabel The label for the accept button
- * @param confirmation The confirmation message to display to the user
- * @param header The header to display to the user
- * @param emitter The emitter to emit the message to when the user confirms the action
- */
- private warnUser(
- messages: ChatMessage[],
- acceptLabel: string,
- confirmation: string,
- header: string,
- emitter: EventEmitter
- ) {
- this.confirm.confirm({
- message: confirmation,
- icon: `pi pi-exclamation-triangle`,
- header: header,
- accept: () => {
- emitter.emit(messages);
- },
- acceptLabel: acceptLabel,
- acceptIcon: `pi pi-check`,
- rejectIcon: `pi pi-times`,
- acceptButtonStyleClass: `p-button-danger`,
- rejectButtonStyleClass: `p-button-secondary`,
- });
- }
-
- onClear() {
- this.confirm.confirm({
- message: 'Are you sure you want to clear the chat history?',
- icon: `pi pi-exclamation-triangle`,
- header: 'Clear Chat History',
- accept: () => {
- this.delete.emit(this.history);
- },
- acceptLabel: 'Clear',
- acceptIcon: `pi pi-check`,
- rejectIcon: `pi pi-times`,
- acceptButtonStyleClass: `p-button-danger`,
- rejectButtonStyleClass: `p-button-secondary`,
- });
- }
-
- countTokens(value: string) {
- this.messageInput.next(value);
- }
}
diff --git a/src/kc_angular/src/app/components/chat-components/message-templates/categorize.message.ts b/src/kc_angular/src/app/components/chat-components/message-templates/categorize.message.ts
new file mode 100644
index 00000000..85ac58d6
--- /dev/null
+++ b/src/kc_angular/src/app/components/chat-components/message-templates/categorize.message.ts
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2024 Rob Royce
+ *
+ * Licensed 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 { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { ChatMessage } from '@app/models/chat.model';
+import {
+ ProjectIdentifiers,
+ ProjectService,
+} from '@services/factory-services/project.service';
+import { take } from 'rxjs/operators';
+import { ProjectTreeNode } from '@app/models/project.tree.model';
+import { KcProject, ProjectCreationRequest } from '@app/models/project.model';
+import { ChatService } from '@services/chat-services/chat.service';
+import { IngestService } from '@services/ingest-services/ingest.service';
+import { NotificationsService } from '@services/user-services/notifications.service';
+import { select } from 'async';
+
+interface ProjectSuggestion {
+ project: KcProject | null | undefined;
+ lineage: string[];
+ details: string;
+ ancestors: ProjectIdentifiers[];
+}
+
+@Component({
+ template: `
+
+
Suggested Projects
+
+ The following projects were suggested for this source:
+
+ `,
+ styles: [
+ `
+ :host {
+ max-width: 100rem !important;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ }
+ .question-choice {
+ cursor: pointer !important;
+ display: flex;
+ flex-direction: column;
+ align-content: center;
+ justify-content: center;
+ align-items: center;
+ text-align: center !important;
+ min-width: 50% !important;
+ height: 5rem;
+ }
+ `,
+ ],
+ animations: [fadeInAndOut, flyInOut, fadeIn],
+})
+export class QuizMessage implements OnChanges {
+ @Input() message!: ChatMessage;
+
+ /* The questions, choices, and answers */
+ questions: QuizQuestion[] = [];
+
+ /* The index of the current question */
+ questionIndex = 0;
+
+ complete = false;
+
+ tryAgain = false;
+
+ private quips = [
+ 'Look at you go, you little genius',
+ "It's big brain time",
+ "You're on fire",
+ "You're a wizard, Harry",
+ 'Take it easy, Einstein',
+ 'How do you know all this stuff?',
+ 'Next stop, Jeopardy',
+ 'Can I borrow your brain for a sec?',
+ 'Your mom would be proud',
+ "I'd give you a high five if I could",
+ ];
+
+ quip: string;
+
+ hint?: string = undefined;
+
+ private audio = {
+ correct: new Audio(),
+ incorrect: new Audio(),
+ complete: new Audio(),
+ };
+
+ constructor(private chat: ChatService) {
+ // Select a random quip to display after the user gets all questions correct
+ function getRandomInt(max: number) {
+ return Math.floor(Math.random() * Math.floor(max));
+ }
+ this.quip = this.quips[getRandomInt(this.quips.length)];
+ this.audio.correct.src = 'assets/sounds/correct.wav';
+ this.audio.incorrect.src = 'assets/sounds/incorrect.wav';
+ this.audio.complete.src = 'assets/sounds/complete.wav';
+ this.audio.correct.load();
+ this.audio.incorrect.load();
+ this.audio.complete.load();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.message) {
+ setTimeout(() => {
+ this.questions = this.parseQuiz(changes.message.currentValue);
+ }, 100);
+ }
+ }
+
+ get numberCorrect() {
+ return this.questions.filter((question) => question.correct).length;
+ }
+
+ private parseQuiz(message: ChatMessage): QuizQuestion[] {
+ const questions: QuizQuestion[] = [];
+ const qaRegex = /Q.*\n+A.*\n+B.*\n+C.*\n+D.*\n+A.*\n*$/gm;
+ const matches = message.text.match(qaRegex);
+
+ if (!matches) {
+ console.warn('No matches found in quiz message: ', message.text);
+ return [];
+ }
+
+ for (const match of matches) {
+ const components = match.split('\n');
+
+ // Remove any blank elements
+ for (let i = 0; i < components.length; i++) {
+ if (components[i] === '') {
+ components.splice(i, 1);
+ }
+ }
+
+ const question: QuizQuestion = {
+ question: components[0].replace(/Q[0-9]*\. /, ''),
+ choices: [components[1], components[2], components[3], components[4]],
+ answer: components[5].replace('Answer: ', '').substring(0, 1),
+ correct: false,
+ answered: false,
+ };
+ questions.push(question);
+ }
+
+ return questions;
+ }
+
+ answer(question: QuizQuestion, choice: string) {
+ question.answered = true;
+
+ // Answer index is the index of the choice in the choices array
+ const answerIndex = question.choices.indexOf(choice);
+
+ // The answer will be either A, B, C, or D
+ const answer = String.fromCharCode(65 + answerIndex);
+
+ // Check if the answer is correct
+ question.correct = answer === question.answer;
+
+ // If the answer is correct, play a sound
+ if (question.correct) {
+ this.audio.correct.play();
+ } else {
+ this.audio.incorrect.play();
+ }
+
+ setTimeout(() => {
+ this.nextQuestion();
+ }, 1650);
+ }
+
+ nextQuestion() {
+ this.hint = undefined;
+
+ if (this.questions.every((question) => question.answered)) {
+ // If all questions were answered correctly, celebrate with a small animation
+ if (this.questions.every((question) => question.correct)) {
+ this.complete = true;
+ this.audio.complete.play();
+ return;
+ } else {
+ this.tryAgain = true;
+ this.audio.incorrect.play();
+ }
+ }
+
+ // Otherwise, go to the next question that has not been answered correctly
+ // Make sure to wrap around to the beginning if necessary
+ let index = (this.questionIndex + 1) % this.questions.length;
+ while (index < this.questions.length && this.questions[index].correct) {
+ index = (index + 1) % this.questions.length;
+ }
+
+ if (this.questions[index].answered) {
+ this.questions[index].answered = false;
+ }
+ this.questionIndex = index;
+ }
+
+ help(question: QuizQuestion) {
+ this.chat.messages$.pipe(take(1)).subscribe((messages) => {
+ const augmented = [
+ ...this.chat.convertToOpenAI(messages),
+ {
+ role: 'system',
+ content:
+ 'The following is a question from a quiz you gave to the user.',
+ },
+ {
+ role: 'system',
+ content: 'Keep in mind it is a multiple choice question.',
+ },
+ {
+ role: 'system',
+ content: `Question: ${question.question}`,
+ },
+ {
+ role: 'system',
+ content: `The following answer is provided strictly for you to paraphrase and help the user figure it out on their own.`,
+ },
+ {
+ role: 'system',
+ content: `${question.answer}`,
+ },
+ {
+ role: 'system',
+ content:
+ 'Do not, under any circumstance, give the user the answer to this question.',
+ },
+ {
+ role: 'system',
+ content: 'Please starting your response with "hint: "',
+ },
+ {
+ role: 'user',
+ content:
+ 'I need help trying to figure out this question, but I do not want you to tell me the answer.',
+ },
+ ];
+ this.chat
+ .post('/chat', { messages: augmented })
+ .pipe(take(1))
+ .subscribe((response) => {
+ this.hint = response;
+ });
+ });
+ }
+}
diff --git a/src/kc_angular/src/app/components/chat-components/message-templates/topic.message.ts b/src/kc_angular/src/app/components/chat-components/message-templates/topic.message.ts
new file mode 100644
index 00000000..ff5757c8
--- /dev/null
+++ b/src/kc_angular/src/app/components/chat-components/message-templates/topic.message.ts
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2024 Rob Royce
+ *
+ * Licensed 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 { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { fadeIn } from '@app/animations';
+import { ChatMessage } from '@app/models/chat.model';
+import { ChatService } from '@services/chat-services/chat.service';
+import { KcProject } from '@app/models/project.model';
+
+export interface TopicDetails {
+ topic: string;
+ description?: string;
+ added?: boolean;
+ keywords?: string[];
+ passages?: string[];
+ projects?: KcProject[];
+}
+
+@Component({
+ selector: 'chat-topic',
+ template: `
+
+
+
+
Topics
+
+
+
+
+
+
+
+
+ {{ topic.topic }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ description }}
+
+
+
+ Click on a topic to learn more
+
+
+
+
+ `,
+ styles: [
+ `
+ :host {
+ width: 100%;
+ }
+ `,
+ ],
+ animations: [fadeIn],
+})
+export class TopicMessage implements OnChanges {
+ @Input() message!: ChatMessage;
+
+ topics: TopicDetails[] = [];
+
+ description?: string = '';
+
+ constructor(private chat: ChatService) {}
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.message) {
+ this.topics = this.parseTopics(this.message.text);
+
+ for (const topic of this.topics) {
+ topic.added =
+ this.message.source?.topics?.includes(topic.topic) ||
+ this.message.project?.topics?.includes(topic.topic);
+ }
+
+ setTimeout(() => {
+ this.onClick(this.topics[0]);
+ });
+ }
+ }
+
+ private parseTopics(text: string): TopicDetails[] {
+ const details: TopicDetails[] = [];
+ let topics: string[] = text.split('\n');
+
+ // Filter out empty strings
+ topics = topics.filter(
+ (topic) => topic.trim() !== '' && topic.startsWith('-')
+ );
+
+ for (const topic of topics) {
+ const split = topic.split(':');
+ const topicName = split[0].trim().substring(1).trim();
+ const description = split[1]?.trim() ?? '';
+ details.push({ topic: topicName, description });
+ }
+
+ return details;
+ }
+
+ addTopic(topic: TopicDetails) {
+ topic.added = true;
+ this.chat.addTopic(topic.topic);
+ }
+
+ onClick(topic: TopicDetails) {
+ this.description =
+ this.description === topic?.description ? '' : topic?.description;
+ }
+}
diff --git a/src/kc_angular/src/app/components/chat.component.ts b/src/kc_angular/src/app/components/chat.component.ts
index 8ed3030b..9f2b5d23 100755
--- a/src/kc_angular/src/app/components/chat.component.ts
+++ b/src/kc_angular/src/app/components/chat.component.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 Rob Royce
+ * Copyright (c) 2023-2024 Rob Royce
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,309 +14,30 @@
* limitations under the License.
*/
-import { Component, ViewChild } from '@angular/core';
-import { DataService } from '@services/user-services/data.service';
-import { finalize, map, take, tap } from 'rxjs/operators';
-import { KnowledgeSource } from '../models/knowledge.source.model';
-import { KcProject } from '../models/project.model';
-import { ContextMenu } from 'primeng/contextmenu';
-import { MenuItem } from 'primeng/api';
+import { Component, OnDestroy } from '@angular/core';
import { ProjectService } from '@services/factory-services/project.service';
import { ChatService } from '@services/chat-services/chat.service';
-import { ProjectChat } from '@services/chat-services/project';
-import { ChatViewComponent } from './chat-components/chat.view.component';
-import { SourceChat } from '@services/chat-services/source';
-import { AgentType, ChatMessage } from '@app/models/chat.model';
-import { SettingsService } from '@services/ipc-services/settings.service';
-import { combineLatest } from 'rxjs';
-import { ChatCompletionMessage } from 'openai/resources/chat/completions';
+import { Subscription } from 'rxjs';
@Component({
selector: 'app-chat',
template: `
-
+
-
-
`,
styles: [``],
})
-export class ChatComponent {
- /* The context menu for the chat view */
- @ViewChild('cm') cm!: ContextMenu;
+export class ChatComponent implements OnDestroy {
+ subscription: Subscription;
- /* The chat view component */
- @ViewChild('chatView') chatView!: ChatViewComponent;
-
- /* The chat history */
- chatHistory: ChatMessage[] = [];
-
- /* The loading state of the chat, used to show the loading bar */
- loading = false;
-
- /* The knowledge sources for the current project */
- sources: KnowledgeSource[] = [];
-
- /* The current project */
- project!: KcProject;
-
- /* The context menu items */
- menuItems: MenuItem[] = [];
-
- showSources = true;
-
- constructor(
- private data: DataService,
- private chat: ChatService,
- private projects: ProjectService,
- private projectChat: ProjectChat,
- private settings: SettingsService,
- private sourceChat: SourceChat
- ) {
- combineLatest([
- settings.app.pipe(map((app) => app.chat.display.sourceMessages)),
- projects.currentProject.pipe(
- map((project) => project || new KcProject('', { value: '' }))
- ),
- ])
- .pipe(
- map(([show, project]) => {
- this.showSources = show;
- this.chatHistory = [];
- this.sources = [];
- this.project = project;
- let sources: KnowledgeSource[] = project.knowledgeSource;
-
- // Get all the sources for the current project and its subprojects
- const subProjects = [...project.subprojects];
-
- while (subProjects.length > 0) {
- const subProject = subProjects.pop();
-
- if (subProject) {
- const sub = this.projects.getProject(subProject);
- if (sub) {
- for (const source of sub.knowledgeSource) {
- sources = [...sources, source];
- }
- for (const sp of sub.subprojects) {
- subProjects.push(sp);
- }
- }
- }
- }
-
- this.sources = sources;
- this.loadHistory();
- })
- )
- .subscribe();
- }
-
- /**
- * Save the chat history for the current project and all sources
- */
- saveHistory() {
- if (this.project) {
- const projectHistory = this.chatHistory.filter(
- (message) => message.project?.id.value === this.project?.id.value
- );
- this.chat.saveChat(projectHistory, this.project?.id);
- }
-
- // Save the individual history for all sources
- let sources = this.chatHistory.map((message) => message.source);
-
- // Get unique sources
- sources = sources.filter((source, index) => {
- return sources.indexOf(source) === index;
+ constructor(private chat: ChatService, private projects: ProjectService) {
+ this.subscription = this.projects.currentProject.subscribe((project) => {
+ this.chat.setTarget({ project: project });
});
-
- for (const source of sources) {
- if (source) {
- const sourceHistory = this.chatHistory.filter(
- (message) => message.source?.id.value === source?.id.value
- );
- this.chat.saveChat(sourceHistory, source.id);
- }
- }
}
- /**
- * Load the chat history for the current project and all sources
- */
- loadHistory() {
- this.loading = true;
- this.chatHistory = [];
-
- if (this.project?.id) {
- const projectChat = this.chat.loadChat(
- this.project.id,
- this.project,
- undefined
- );
- for (const message of projectChat) {
- this.chatHistory.push(message);
- }
- }
-
- // Load the source history
- if (this.showSources) {
- for (const source of this.sources) {
- const sourceChat = this.chat.loadChat(source.id, undefined, source);
- for (const message of sourceChat) {
- this.chatHistory.push(message);
- }
- }
- }
-
- // Sort history by timestamp
- this.chatHistory = this.chatHistory.sort(
- (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
- );
- this.loading = false;
- }
-
- /**
- * Create a new chat message and send it to the chat service on submit
- * @param $event The text of the message
- */
- onSubmit($event: string) {
- this.loading = true;
-
- if (this.chatHistory.length === 0) {
- this.chatHistory = [];
- }
-
- this.chatHistory.push(
- this.chat.createMessage(
- AgentType.User,
- AgentType.Project,
- $event,
- this.project
- )
- );
- this.chatView.scroll();
-
- this.projectChat
- .send(this.project, this.chatHistory)
- .pipe(
- take(1),
- tap((response: ChatCompletionMessage) => {
- if (response.content) {
- this.chatHistory.push(
- this.chat.createMessage(
- AgentType.Project,
- AgentType.User,
- response.content,
- this.project
- )
- );
- }
- this.saveHistory();
- this.chatView.scroll();
- }),
- finalize(() => {
- // If the user message was not responded to, remove it from the history
- if (
- this.chatHistory[this.chatHistory.length - 1].sender !==
- AgentType.Project
- ) {
- // Remove first element
- this.chatHistory.shift();
- }
- this.loading = false;
- })
- )
- .subscribe();
- }
-
- /**
- * Delete a message from the history
- * @param messages The message(s) to delete
- */
- onDeleteMessage(messages: ChatMessage[]) {
- // Remove messages from the history and save
- this.chatHistory = this.chatHistory.filter(
- (message) => !messages.includes(message)
- );
- }
-
- /**
- * Regenerate a message by sending it and its history to the chat service
- * @param message The message to regenerate
- */
- regenerateMessage(message: ChatMessage) {
- if (!this.project) {
- return;
- }
-
- this.loading = true;
-
- /* Use the message index to get the history up to that point
- * Source history should only include messages to/from the same source
- * Project history should include messages from the project and any sources that are part of the project */
- const historySlice = this.chat.getHistory(
- message,
- this.chatHistory,
- false,
- !message.project
- );
-
- /* Create a new message with the same text as the message that was responded to */
- const promptMessage = historySlice[historySlice.length - 1];
-
- if (message.project) {
- this.projectChat
- .send(message.project, historySlice, promptMessage.text)
- .pipe(
- take(1),
- tap((response: ChatCompletionMessage) => {
- if (response.content) {
- message.text = response.content;
- message.regenerated = true;
- this.saveHistory();
- }
- }),
- finalize(() => {
- this.loading = false;
- })
- )
- .subscribe();
- } else if (message.source) {
- this.sourceChat
- .send(message.source, historySlice, promptMessage.text)
- .pipe(
- take(1),
- tap((response: ChatCompletionMessage) => {
- if (response.content) {
- message.text = response.content;
- message.regenerated = true;
- this.saveHistory();
- }
- }),
- finalize(() => {
- this.loading = false;
- })
- )
- .subscribe();
- }
+ ngOnDestroy() {
+ this.subscription.unsubscribe();
}
}
diff --git a/src/kc_angular/src/app/components/graph.component.ts b/src/kc_angular/src/app/components/graph.component.ts
index 2bb7df8b..52bd84c8 100755
--- a/src/kc_angular/src/app/components/graph.component.ts
+++ b/src/kc_angular/src/app/components/graph.component.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 Rob Royce
+ * Copyright (c) 2023-2024 Rob Royce
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ import { createGraph } from '@app/workers/graph.worker';
diff --git a/src/kc_angular/src/app/components/home.component.ts b/src/kc_angular/src/app/components/home.component.ts
index 8b7eabb3..eb8d7b25 100755
--- a/src/kc_angular/src/app/components/home.component.ts
+++ b/src/kc_angular/src/app/components/home.component.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022-2023 Rob Royce
+ * Copyright (c) 2022-2024 Rob Royce
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,7 +40,7 @@ import { finalize, map, take, takeUntil, tap } from 'rxjs/operators';
template: `