-
-
Notifications
You must be signed in to change notification settings - Fork 48
Ask AI feature #447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Ask AI feature #447
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
96e11f1
Ask AI feature
Blaumaus 5ac2fb7
fix incorrect tool calling
Blaumaus c9ba7ce
style tool calls differently
Blaumaus 2ddfa8f
thinking process
Blaumaus c764c4c
medium reasoning effort
Blaumaus d24f3da
better autoscroll
Blaumaus ea2a527
better UI
Blaumaus cfa98e6
capabilities tooltip
Blaumaus 7e50f77
add some logging
Blaumaus c3eead1
minimalistic tools display
Blaumaus e077bbe
google vertex provider
Blaumaus 0a7204a
fix colours
Blaumaus d785912
use claude haiku
Blaumaus 7ed290f
askAi -> ai
Blaumaus 1f67ba3
AI chat history
Blaumaus 1672b3e
a few improvements to the AI chat
Blaumaus 4b59806
fix lint
Blaumaus 79efbc2
AI fixes
Blaumaus 9471b6a
security improvements
Blaumaus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import { Injectable } from '@nestjs/common' | ||
| import { InjectRepository } from '@nestjs/typeorm' | ||
| import { Repository, FindManyOptions, FindOneOptions } from 'typeorm' | ||
| import { AiChat, ChatMessage } from './entity/ai-chat.entity' | ||
|
|
||
| @Injectable() | ||
| export class AiChatService { | ||
| constructor( | ||
| @InjectRepository(AiChat) | ||
| private aiChatRepository: Repository<AiChat>, | ||
| ) {} | ||
|
|
||
| async findOne(options: FindOneOptions<AiChat>): Promise<AiChat | null> { | ||
| return this.aiChatRepository.findOne(options) | ||
| } | ||
|
|
||
| async find(options: FindManyOptions<AiChat>): Promise<AiChat[]> { | ||
| return this.aiChatRepository.find(options) | ||
| } | ||
|
|
||
| async findRecentByProject( | ||
| projectId: string, | ||
| userId: string | null, | ||
| limit: number = 5, | ||
| ): Promise<AiChat[]> { | ||
| const queryBuilder = this.aiChatRepository | ||
| .createQueryBuilder('chat') | ||
| .where('chat.projectId = :projectId', { projectId }) | ||
| .orderBy('chat.updated', 'DESC') | ||
| .take(limit) | ||
|
|
||
| if (userId) { | ||
| queryBuilder.andWhere('(chat.userId = :userId OR chat.userId IS NULL)', { | ||
| userId, | ||
| }) | ||
| } | ||
|
|
||
| return queryBuilder.getMany() | ||
| } | ||
|
|
||
| async findAllByProject( | ||
| projectId: string, | ||
| userId: string | null, | ||
| skip: number = 0, | ||
| take: number = 20, | ||
| ): Promise<{ chats: AiChat[]; total: number }> { | ||
| const queryBuilder = this.aiChatRepository | ||
| .createQueryBuilder('chat') | ||
| .where('chat.projectId = :projectId', { projectId }) | ||
| .orderBy('chat.updated', 'DESC') | ||
| .skip(skip) | ||
| .take(take) | ||
|
|
||
| if (userId) { | ||
| queryBuilder.andWhere('(chat.userId = :userId OR chat.userId IS NULL)', { | ||
| userId, | ||
| }) | ||
| } | ||
|
|
||
| const [chats, total] = await queryBuilder.getManyAndCount() | ||
| return { chats, total } | ||
| } | ||
|
|
||
| async create(data: { | ||
| projectId: string | ||
| userId: string | null | ||
| messages: ChatMessage[] | ||
| name?: string | ||
| }): Promise<AiChat> { | ||
| const chat = this.aiChatRepository.create({ | ||
| project: { id: data.projectId }, | ||
| user: data.userId ? { id: data.userId } : null, | ||
| messages: data.messages, | ||
| name: data.name || this.generateChatName(data.messages), | ||
| }) | ||
| return this.aiChatRepository.save(chat) | ||
| } | ||
|
|
||
| async update( | ||
| id: string, | ||
| data: { messages?: ChatMessage[]; name?: string }, | ||
| ): Promise<AiChat | null> { | ||
| const chat = await this.aiChatRepository.findOne({ where: { id } }) | ||
| if (!chat) return null | ||
|
|
||
| if (data.messages) { | ||
| const previousMessages = chat.messages | ||
| chat.messages = data.messages | ||
| // Update name if not manually set and we have a new first user message | ||
| if (!chat.name || chat.name === this.generateChatName(previousMessages)) { | ||
| chat.name = this.generateChatName(data.messages) | ||
| } | ||
| } | ||
| if (data.name !== undefined) { | ||
| chat.name = data.name | ||
| } | ||
|
|
||
| return this.aiChatRepository.save(chat) | ||
| } | ||
|
|
||
| async delete(id: string): Promise<boolean> { | ||
| const result = await this.aiChatRepository.delete(id) | ||
| return (result.affected ?? 0) > 0 | ||
| } | ||
|
|
||
| private generateChatName(messages: ChatMessage[]): string { | ||
| // Use the first user message as the chat name | ||
| const firstUserMessage = messages.find(m => m.role === 'user') | ||
| if (firstUserMessage) { | ||
| const content = firstUserMessage.content.trim() | ||
| // Truncate to reasonable length | ||
| return content.length > 100 ? content.slice(0, 97) + '...' : content | ||
| } | ||
| return 'New conversation' | ||
| } | ||
|
|
||
| async verifyAccess( | ||
| chatId: string, | ||
| projectId: string, | ||
| userId: string | null, | ||
| ): Promise<AiChat | null> { | ||
| const queryBuilder = this.aiChatRepository | ||
| .createQueryBuilder('chat') | ||
| .where('chat.id = :chatId', { chatId }) | ||
| .andWhere('chat.projectId = :projectId', { projectId }) | ||
|
|
||
| if (userId) { | ||
| queryBuilder.andWhere('(chat.userId = :userId OR chat.userId IS NULL)', { | ||
| userId, | ||
| }) | ||
| } | ||
|
|
||
| return queryBuilder.getOne() | ||
| } | ||
|
|
||
| /** | ||
| * Verify that a chat belongs to a project (without checking user ownership). | ||
| * Used for shared chat links where anyone who can view the project can access the chat. | ||
| */ | ||
| async verifyProjectAccess( | ||
| chatId: string, | ||
| projectId: string, | ||
| ): Promise<AiChat | null> { | ||
| return this.aiChatRepository.findOne({ | ||
| where: { | ||
| id: chatId, | ||
| project: { id: projectId }, | ||
| }, | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.