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