Skip to content

Commit 4d541cf

Browse files
committed
Add temp, topK, topP etc settings for chat
1 parent c35ab2f commit 4d541cf

29 files changed

+702
-210
lines changed

frontend/src/app/core/user/user.service.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { HttpClient } from '@angular/common/http';
22
import { inject, Injectable } from '@angular/core';
33
import { User } from 'app/core/user/user.types';
4-
import { catchError, Observable, ReplaySubject, tap, throwError } from 'rxjs';
4+
import { catchError, Observable, BehaviorSubject, tap, throwError, mergeMap } from 'rxjs';
55

66
@Injectable({ providedIn: 'root' })
77
export class UserService {
88
private _httpClient = inject(HttpClient);
9-
private _user: ReplaySubject<User> = new ReplaySubject<User>(1);
9+
private _user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
1010

1111
// -----------------------------------------------------------------------------------------------------
1212
// @ Accessors
@@ -34,6 +34,13 @@ export class UserService {
3434
* Get the current signed-in user data
3535
*/
3636
get(): Observable<User> {
37+
// Return the current value if it exists
38+
const currentUser = this._user.getValue();
39+
if (currentUser) {
40+
return this.user$;
41+
}
42+
43+
// Fetch from server if no current value
3744
return this._httpClient.get<User>(`/api/profile/view`).pipe(
3845
tap((user) => {
3946
user = (user as any).data
@@ -42,7 +49,8 @@ export class UserService {
4249
catchError(error => {
4350
console.error('Error loading profile', error);
4451
return throwError(() => new Error('Error loading profile'));
45-
})
52+
}),
53+
mergeMap(value => this.user$)
4654
);
4755
}
4856

@@ -51,10 +59,11 @@ export class UserService {
5159
*
5260
* @param user
5361
*/
54-
update(user: User): Observable<User> {
55-
return this._httpClient.patch<User>('/api/profile/update', { user }).pipe(
62+
update(user: Partial<User>): Observable<User> {
63+
return this._httpClient.post<User>('/api/profile/update', { user }).pipe(
5664
tap((response) => {
57-
this._user.next(response);
65+
response = (response as any).data;
66+
this._user.next({...response});
5867
})
5968
);
6069
}
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1+
export interface GenerateOptions {
2+
/** Temperature controls randomness in token selection (0-2) */
3+
temperature?: number;
4+
/** Top P controls diversity via nucleus sampling (0-1) */
5+
topP?: number;
6+
topK?: number;
7+
/** Presence penalty reduces repetition (-2.0 to 2.0) */
8+
presencePenalty?: number;
9+
/** Frequency penalty reduces repetition (-2.0 to 2.0) */
10+
frequencyPenalty?: number;
11+
}
12+
113
export interface User {
214
id: string;
315
name: string;
416
email: string;
517
avatar?: string;
618
status?: string;
7-
defaultChatLlmId?: string;
19+
20+
chat?: GenerateOptions & {
21+
enabledLLMs: Record<string, boolean>;
22+
defaultLLM: string;
23+
};
824
}

frontend/src/app/modules/chat/chat-info/chat-info.component.html

Lines changed: 46 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,68 +6,65 @@
66
<button mat-icon-button (click)="drawer.close()">
77
<mat-icon [svgIcon]="'heroicons_outline:x-mark'"></mat-icon>
88
</button>
9-
<div class="ml-2 text-lg font-medium">Chat info</div>
9+
<div class="ml-2 text-lg font-medium">Chat Settings</div>
1010
</div>
1111

12-
<div class="overflow-y-auto">
13-
<div class="px-7 py-10">
14-
<!-- Media
15-
<div class="text-lg font-medium">Media</div>
16-
<div class="mt-4 grid grid-cols-4 gap-1">
17-
@for (media of chat.contact.attachments.media; track media) {
18-
<img class="h-20 rounded object-cover" [src]="media" />
19-
}
20-
</div>
21-
-->
22-
<!-- Details
23-
<div class="mt-10 space-y-4">
24-
<div class="mb-3 text-lg font-medium">Details</div>
25-
@if (chat.contact.details.emails.length) {
26-
<div>
27-
<div class="text-secondary font-medium">Email</div>
28-
<div class="">
29-
{{ chat.contact.details.emails[0].email }}
30-
</div>
31-
</div>
32-
}
33-
@if (chat.contact.details.phoneNumbers.length) {
12+
<div class="overflow-y-auto" [class.opacity-50]="loading">
13+
<div class="px-7 py-6">
14+
<div class="mt-4 space-y-4">
15+
<div class="mb-2 text-lg font-medium">Message Generation Settings</div>
16+
17+
<div class="space-y-1">
3418
<div>
35-
<div class="text-secondary font-medium">
36-
Phone number
37-
</div>
38-
<div class="">
39-
{{
40-
chat.contact.details.phoneNumbers[0].phoneNumber
41-
}}
42-
</div>
19+
<label class="text-secondary font-medium">Temperature</label>
20+
<span class="pl-2 font-bold text-secondary">{{settings.temperature}}</span>
21+
<mat-slider [min]="0" [max]="2" [step]="0.1">
22+
<input matSliderThumb [(ngModel)]="settings.temperature" (change)="onSettingChange()">
23+
</mat-slider>
4324
</div>
44-
}
45-
@if (chat.contact.details.title) {
25+
4626
<div>
47-
<div class="text-secondary font-medium">Title</div>
48-
<div class="">{{ chat.contact.details.title }}</div>
27+
<label class="text-secondary font-medium">Top P</label>
28+
<span class="pl-2 font-bold text-secondary">{{settings.topP}}</span>
29+
<mat-slider [min]="0" [max]="1" [step]="0.05">
30+
<input matSliderThumb [(ngModel)]="settings.topP" (change)="onSettingChange()">
31+
</mat-slider>
4932
</div>
50-
}
51-
@if (chat.contact.details.company) {
33+
5234
<div>
53-
<div class="text-secondary font-medium">Company</div>
54-
<div class="">{{ chat.contact.details.company }}</div>
35+
<label class="text-secondary font-medium">Top K</label>
36+
<span class="pl-2 font-bold text-secondary">{{settings.topK}}</span>
37+
<mat-slider [min]="1" [max]="60" [step]="1">
38+
<input matSliderThumb [(ngModel)]="settings.topK" (change)="onSettingChange()">
39+
</mat-slider>
5540
</div>
56-
}
57-
@if (chat.contact.details.birthday) {
41+
5842
<div>
59-
<div class="text-secondary font-medium">Birthday</div>
60-
<div class="">{{ chat.contact.details.birthday }}</div>
43+
<label class="text-secondary font-medium">Presence Penalty</label>
44+
<span class="pl-2 font-bold text-secondary">{{settings.presencePenalty}}</span>
45+
<mat-slider [min]="0" [max]="2" [step]="0.1">
46+
<input matSliderThumb [(ngModel)]="settings.presencePenalty" (change)="onSettingChange()">
47+
</mat-slider>
6148
</div>
62-
}
63-
@if (chat.contact.details.address) {
49+
6450
<div>
65-
<div class="text-secondary font-medium">Address</div>
66-
<div class="">{{ chat.contact.details.address }}</div>
51+
<label class="text-secondary font-medium">Frequency Penalty</label>
52+
<span class="pl-2 font-bold text-secondary">{{settings.frequencyPenalty}}</span>
53+
<mat-slider [min]="0" [max]="2" [step]="0.1">
54+
<input matSliderThumb [(ngModel)]="settings.frequencyPenalty" (change)="onSettingChange()">
55+
</mat-slider>
6756
</div>
68-
}
57+
</div>
58+
</div>
59+
60+
<div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded">
61+
{{ error }}
62+
</div>
63+
64+
<div *ngIf="loading" class="mt-4 flex items-center justify-center">
65+
<mat-spinner diameter="24"></mat-spinner>
66+
<span class="ml-2">Saving settings...</span>
6967
</div>
70-
-->
7168
</div>
7269
</div>
7370
</div>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { ChatInfoComponent } from './chat-info.component';
3+
import { UserService } from 'app/core/user/user.service';
4+
import { BehaviorSubject, of } from 'rxjs';
5+
import { User } from 'app/core/user/user.types';
6+
import { MatDrawer } from '@angular/material/sidenav';
7+
import { HarnessLoader } from '@angular/cdk/testing';
8+
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
9+
import { MatSliderHarness } from '@angular/material/slider/testing';
10+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
11+
12+
describe('ChatInfoComponent', () => {
13+
let component: ChatInfoComponent;
14+
let fixture: ComponentFixture<ChatInfoComponent>;
15+
let loader: HarnessLoader;
16+
let mockUserService: jasmine.SpyObj<UserService>;
17+
let mockUser$: BehaviorSubject<User>;
18+
19+
beforeEach(async () => {
20+
// Create mock user with default chat settings
21+
const mockUser: User = {
22+
id: '1',
23+
name: 'Test User',
24+
email: 'test@test.com',
25+
chat: {
26+
temperature: 0.7,
27+
enabledLLMs: {},
28+
defaultLLM: 'test-llm'
29+
}
30+
};
31+
32+
// Setup mock user service
33+
mockUser$ = new BehaviorSubject<User>(mockUser);
34+
mockUserService = jasmine.createSpyObj('UserService', ['update']);
35+
mockUserService.user$ = mockUser$.asObservable();
36+
mockUserService.update.and.returnValue(of(mockUser));
37+
38+
await TestBed.configureTestingModule({
39+
imports: [
40+
ChatInfoComponent,
41+
NoopAnimationsModule
42+
],
43+
providers: [
44+
{ provide: UserService, useValue: mockUserService },
45+
{ provide: MatDrawer, useValue: { close: () => {} } }
46+
]
47+
}).compileComponents();
48+
49+
fixture = TestBed.createComponent(ChatInfoComponent);
50+
component = fixture.componentInstance;
51+
loader = TestbedHarnessEnvironment.loader(fixture);
52+
fixture.detectChanges();
53+
});
54+
55+
it('should create', () => {
56+
expect(component).toBeTruthy();
57+
});
58+
59+
it('should load initial settings from user service', async () => {
60+
// Get all slider harnesses
61+
const sliders = await loader.getAllHarnesses(MatSliderHarness);
62+
63+
// Verify initial values match mock user settings
64+
expect(await sliders[0].getValue()).toBe(0.7); // temperature
65+
expect(await sliders[1].getValue()).toBe(0.9); // topP
66+
expect(await sliders[2].getValue()).toBe(0.5); // presencePenalty
67+
expect(await sliders[3].getValue()).toBe(0.5); // frequencyPenalty
68+
});
69+
70+
it('should update settings when sliders change', async () => {
71+
// Get temperature slider and change its value
72+
const temperatureSlider = await loader.getHarness(MatSliderHarness);
73+
await temperatureSlider.setValue(1.5);
74+
75+
// Verify component state was updated
76+
expect(component.settings.temperature).toBe(1.5);
77+
});
78+
79+
it('should save settings on component destroy', () => {
80+
// Modify settings
81+
component.settings.temperature = 1.5;
82+
83+
// Trigger component destruction
84+
component.ngOnDestroy();
85+
86+
// Verify settings were saved
87+
expect(mockUserService.update).toHaveBeenCalledWith({
88+
chat: jasmine.objectContaining({
89+
temperature: 1.5
90+
})
91+
});
92+
});
93+
});

frontend/src/app/modules/chat/chat-info/chat-info.component.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,94 @@ import {
22
ChangeDetectionStrategy,
33
Component,
44
Input,
5+
OnDestroy,
56
ViewEncapsulation,
67
} from '@angular/core';
78
import { MatButtonModule } from '@angular/material/button';
89
import { MatIconModule } from '@angular/material/icon';
10+
import { MatSliderModule } from '@angular/material/slider';
11+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
12+
import { FormsModule } from '@angular/forms';
13+
import { CommonModule } from '@angular/common';
914
import { MatDrawer } from '@angular/material/sidenav';
1015
import { Chat } from 'app/modules/chat/chat.types';
16+
import { User } from 'app/core/user/user.types';
17+
import { UserService } from 'app/core/user/user.service';
18+
import { EMPTY, Subject, catchError, takeUntil, finalize } from 'rxjs';
1119

1220
@Component({
1321
selector: 'chat-info',
1422
templateUrl: './chat-info.component.html',
1523
encapsulation: ViewEncapsulation.None,
1624
changeDetection: ChangeDetectionStrategy.OnPush,
1725
standalone: true,
18-
imports: [MatButtonModule, MatIconModule],
26+
imports: [
27+
CommonModule,
28+
MatButtonModule,
29+
MatIconModule,
30+
MatSliderModule,
31+
MatProgressSpinnerModule,
32+
FormsModule
33+
],
1934
})
20-
export class ChatInfoComponent {
35+
export class ChatInfoComponent implements OnDestroy {
2136
@Input() chat: Chat;
2237
@Input() drawer: MatDrawer;
38+
39+
settings: User['chat'];
40+
loading = false;
41+
error: string | null = null;
42+
private destroy$ = new Subject<void>();
43+
44+
constructor(private userService: UserService) {
45+
this.userService.user$
46+
.pipe(takeUntil(this.destroy$))
47+
.subscribe(user => {
48+
this.settings = { ...user.chat };
49+
});
50+
}
51+
52+
/**
53+
* Saves the current chat settings to the user profile
54+
* Handles loading state and error display
55+
*/
56+
/**
57+
* Saves the current chat settings to the user profile
58+
* Handles loading state and error display
59+
*/
60+
private saveSettings(): void {
61+
if (!this.settings) {
62+
return;
63+
}
64+
65+
this.loading = true;
66+
this.error = null;
67+
68+
this.userService.update({
69+
chat: this.settings
70+
}).pipe(
71+
takeUntil(this.destroy$),
72+
catchError((error) => {
73+
this.error = error.error?.error || 'Failed to save settings';
74+
console.error('Failed to save chat settings:', error);
75+
return EMPTY;
76+
}),
77+
finalize(() => {
78+
this.loading = false;
79+
})
80+
).subscribe();
81+
}
2382

2483
/**
25-
* Constructor
84+
* Handler for slider value changes
85+
* Triggers immediate save of updated settings
2686
*/
27-
constructor() {}
87+
onSettingChange(): void {
88+
this.saveSettings();
89+
}
90+
91+
ngOnDestroy(): void {
92+
this.destroy$.next();
93+
this.destroy$.complete();
94+
}
2895
}

0 commit comments

Comments
 (0)