1+ using AIStudio . Chat ;
2+ using AIStudio . Components ;
3+ using AIStudio . Provider ;
4+
5+ using Microsoft . AspNetCore . Components ;
6+ using Microsoft . AspNetCore . Components . Web ;
7+
8+ using Timer = System . Timers . Timer ;
9+
10+ namespace AIStudio . Pages ;
11+
12+ public partial class Writer : MSGComponentBase , IAsyncDisposable
13+ {
14+ [ Inject ]
15+ private ILogger < Chat > Logger { get ; init ; } = null ! ;
16+
17+ private static readonly Dictionary < string , object ? > USER_INPUT_ATTRIBUTES = new ( ) ;
18+ private readonly Timer typeTimer = new ( TimeSpan . FromMilliseconds ( 1_500 ) ) ;
19+
20+ private MudTextField < string > textField = null ! ;
21+ private AIStudio . Settings . Provider providerSettings ;
22+ private ChatThread ? chatThread ;
23+ private bool isStreaming ;
24+ private string userInput = string . Empty ;
25+ private string userDirection = string . Empty ;
26+ private string suggestion = string . Empty ;
27+
28+ #region Overrides of ComponentBase
29+
30+ protected override async Task OnInitializedAsync ( )
31+ {
32+ this . ApplyFilters ( [ ] , [ ] ) ;
33+ this . SettingsManager . InjectSpellchecking ( USER_INPUT_ATTRIBUTES ) ;
34+ this . typeTimer . Elapsed += async ( _ , _ ) => await this . InvokeAsync ( this . GetSuggestions ) ;
35+ this . typeTimer . AutoReset = false ;
36+
37+ await base . OnInitializedAsync ( ) ;
38+ }
39+
40+ #endregion
41+
42+ #region Overrides of MSGComponentBase
43+
44+ public override Task ProcessIncomingMessage < T > ( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
45+ {
46+ return Task . CompletedTask ;
47+ }
48+
49+ public override Task < TResult ? > ProcessMessageWithResult < TPayload , TResult > ( ComponentBase ? sendingComponent , Event triggeredEvent , TPayload ? data ) where TResult : default where TPayload : default
50+ {
51+ return Task . FromResult ( default ( TResult ) ) ;
52+ }
53+
54+ #endregion
55+
56+ private bool IsProviderSelected => this . providerSettings . UsedLLMProvider != LLMProviders . NONE ;
57+
58+ private async Task InputKeyEvent ( KeyboardEventArgs keyEvent )
59+ {
60+ var key = keyEvent . Code . ToLowerInvariant ( ) ;
61+ var isTab = key is "tab" ;
62+ var isModifier = keyEvent . AltKey || keyEvent . CtrlKey || keyEvent . MetaKey || keyEvent . ShiftKey ;
63+
64+ if ( isTab && ! isModifier )
65+ {
66+ await this . textField . FocusAsync ( ) ;
67+ this . AcceptNextWord ( ) ;
68+ return ;
69+ }
70+
71+ if ( isTab && isModifier )
72+ {
73+ await this . textField . FocusAsync ( ) ;
74+ this . AcceptEntireSuggestion ( ) ;
75+ return ;
76+ }
77+
78+ if ( ! isModifier )
79+ {
80+ this . typeTimer . Stop ( ) ;
81+ this . typeTimer . Start ( ) ;
82+ }
83+ }
84+
85+ private async Task GetSuggestions ( )
86+ {
87+ if ( ! this . IsProviderSelected )
88+ return ;
89+
90+ this . chatThread ??= new ( )
91+ {
92+ WorkspaceId = Guid . Empty ,
93+ ChatId = Guid . NewGuid ( ) ,
94+ Name = string . Empty ,
95+ Seed = 798798 ,
96+ SystemPrompt = """
97+ You are an assistant who helps with writing documents. You receive a sample
98+ from a document as input. As output, you provide how the begun sentence could
99+ continue. You give exactly one variant, not multiple. If the current sentence
100+ is complete, you provide an empty response. You do not ask questions, and you
101+ do not repeat the task.
102+ """ ,
103+ Blocks = [ ] ,
104+ } ;
105+
106+ var time = DateTimeOffset . Now ;
107+ this . chatThread . Blocks . Clear ( ) ;
108+ this . chatThread . Blocks . Add ( new ContentBlock
109+ {
110+ Time = time ,
111+ ContentType = ContentType . TEXT ,
112+ Role = ChatRole . USER ,
113+ Content = new ContentText
114+ {
115+ // We use the maximum 160 characters from the end of the text:
116+ Text = this . userInput . Length > 160 ? this . userInput [ ^ 160 ..] : this . userInput ,
117+ } ,
118+ } ) ;
119+
120+ var aiText = new ContentText
121+ {
122+ // We have to wait for the remote
123+ // for the content stream:
124+ InitialRemoteWait = true ,
125+ } ;
126+
127+ this . chatThread ? . Blocks . Add ( new ContentBlock
128+ {
129+ Time = time ,
130+ ContentType = ContentType . TEXT ,
131+ Role = ChatRole . AI ,
132+ Content = aiText ,
133+ } ) ;
134+
135+ this . isStreaming = true ;
136+ this . StateHasChanged ( ) ;
137+
138+ await aiText . CreateFromProviderAsync ( this . providerSettings . CreateProvider ( this . Logger ) , this . SettingsManager , this . providerSettings . Model , this . chatThread ) ;
139+ this . suggestion = aiText . Text ;
140+
141+ this . isStreaming = false ;
142+ this . StateHasChanged ( ) ;
143+ }
144+
145+ private void AcceptEntireSuggestion ( )
146+ {
147+ if ( this . userInput . Last ( ) != ' ' )
148+ this . userInput += ' ' ;
149+
150+ this . userInput += this . suggestion ;
151+ this . suggestion = string . Empty ;
152+ this . StateHasChanged ( ) ;
153+ }
154+
155+ private void AcceptNextWord ( )
156+ {
157+ var words = this . suggestion . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
158+ if ( words . Length == 0 )
159+ return ;
160+
161+ if ( this . userInput . Last ( ) != ' ' )
162+ this . userInput += ' ' ;
163+
164+ this . userInput += words [ 0 ] + ' ' ;
165+ this . suggestion = string . Join ( ' ' , words . Skip ( 1 ) ) ;
166+ this . StateHasChanged ( ) ;
167+ }
168+
169+ #region Implementation of IAsyncDisposable
170+
171+ public ValueTask DisposeAsync ( )
172+ {
173+ return ValueTask . CompletedTask ;
174+ }
175+
176+ #endregion
177+ }
0 commit comments