arcane-os 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -100,6 +100,7 @@
100
100
  }
101
101
 
102
102
  .chat_area[data-conversation-complete="true"] .conversation_timebox,
103
+ .chat_area[data-conversation-complete="true"] .ai_activation,
103
104
  .chat_area[data-conversation-complete="true"] .chat_input_area,
104
105
  .chat_area[data-conversation-complete="true"] > #speech,
105
106
  .chat_area[data-conversation-complete="true"] > .controls{
@@ -209,6 +210,63 @@
209
210
  box-shadow: var(--arcane-shadow-small, 0 .35rem 1rem var(--shadow-color));
210
211
  }
211
212
 
213
+ .ai_activation {
214
+ display:flex;
215
+ flex:0 0 auto;
216
+ box-sizing:border-box;
217
+ width:100%;
218
+ padding:var(--arcane-space-3, .75rem);
219
+ align-items:center;
220
+ justify-content:space-between;
221
+ gap:var(--arcane-space-3, .75rem);
222
+ border:1px solid var(--arcane-border, var(--border-color));
223
+ border-radius:var(--arcane-chat-radius);
224
+ background:var(--arcane-surface-raised, var(--modal-background));
225
+ color:var(--text-color);
226
+ }
227
+
228
+ .ai_activation[hidden] {
229
+ display:none;
230
+ }
231
+
232
+ .ai_activation_copy {
233
+ min-width:0;
234
+ }
235
+
236
+ .ai_activation_copy strong,
237
+ .ai_activation_copy span {
238
+ display:block;
239
+ overflow-wrap:anywhere;
240
+ }
241
+
242
+ .ai_activation_copy span {
243
+ margin-block-start:.22rem;
244
+ font-size:.9rem;
245
+ }
246
+
247
+ .ai_activation button {
248
+ flex:0 0 auto;
249
+ min-height:2.65rem;
250
+ padding:.55rem .8rem;
251
+ border:1px solid var(--arcane-action, var(--primary-color));
252
+ border-radius:var(--arcane-radius-medium, .7rem);
253
+ background:var(--arcane-action, var(--primary-color));
254
+ color:var(--arcane-action-text, var(--button-text-color, var(--text-color)));
255
+ font:inherit;
256
+ font-weight:600;
257
+ cursor:pointer;
258
+ }
259
+
260
+ .ai_activation button:focus-visible {
261
+ outline:.2rem solid var(--arcane-focus, var(--focus-color));
262
+ outline-offset:.18rem;
263
+ }
264
+
265
+ .ai_activation button:disabled {
266
+ cursor:not-allowed;
267
+ opacity:.65;
268
+ }
269
+
212
270
  .chat_input {
213
271
  box-sizing: border-box;
214
272
  width: 100%;
@@ -421,6 +479,20 @@
421
479
  >Starting the elapsed timer.</span>
422
480
  </section>
423
481
  <ul type='text' class='chat_output' id='chat_output' tabindex='-1' aria-label='Conversation transcript'></ul>
482
+ <section
483
+ id="ai_activation"
484
+ class="ai_activation"
485
+ role="status"
486
+ aria-live="polite"
487
+ aria-busy="false"
488
+ hidden
489
+ >
490
+ <span class="ai_activation_copy">
491
+ <strong id="ai_activation_title">Language model not active</strong>
492
+ <span id="ai_activation_status">Start the selected language model to enable chat.</span>
493
+ </span>
494
+ <button type="button" id="ai_activation_button">Start language model</button>
495
+ </section>
424
496
  <div class="chat_input_area">
425
497
  <textarea type='text' class='chat_input' id='chat_input'></textarea>
426
498
  <button type="button" id="chat_upload" class="chat_upload" title="Upload files" aria-label="Upload files">
@@ -624,11 +696,15 @@
624
696
  const conversationTimeboxPanel=shadowRoot.querySelector('#conversation_timebox');
625
697
  const conversationTimeboxElapsed=shadowRoot.querySelector('#conversation_timebox_elapsed');
626
698
  const conversationTimeboxStatus=shadowRoot.querySelector('#conversation_timebox_status');
699
+ const aiActivationPanel=shadowRoot.querySelector('#ai_activation');
700
+ const aiActivationTitle=shadowRoot.querySelector('#ai_activation_title');
701
+ const aiActivationStatus=shadowRoot.querySelector('#ai_activation_status');
702
+ const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
627
703
  const [
628
704
  {default:MD},
629
705
  {default:FileEntity},
630
706
  {ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
631
- {subscribeAIRuntimeState}
707
+ {requestAIRuntimeIntent,subscribeAIRuntimeState}
632
708
  ]=await Promise.all([
633
709
  import('../modules/MD.js'),
634
710
  import('../entities/File.js'),
@@ -663,6 +739,18 @@
663
739
  console.warn('html-import chat.html `host.languageChanged` method not implemented.\n\nThis can also be done by listening for the "chat-language-changed" event.\n\nIt is still best practice to implement the `.languageChanged` method atleast with an empty function.\n\n')
664
740
  }
665
741
 
742
+ host.requestAIActivation=host.requestAIActivation||function requestAIActivation(intent){
743
+ return requestAIRuntimeIntent(intent);
744
+ }
745
+
746
+ const aiActivationController=createAIActivationController({
747
+ host,
748
+ panel:aiActivationPanel,
749
+ title:aiActivationTitle,
750
+ status:aiActivationStatus,
751
+ button:aiActivationButton
752
+ });
753
+
666
754
  host.streamMessage=streamMessage;
667
755
  host.setMessageProgress=setMessageProgress;
668
756
  host.setAIAvailability=setAIAvailability;
@@ -861,7 +949,189 @@
861
949
  send.setAttribute('aria-label',send.title);
862
950
  }
863
951
 
952
+ function createAIActivationController({host,panel,title,status,button,EventClass=CustomEvent}){
953
+ let role=null;
954
+ let requestPending=false;
955
+ let requestError='';
956
+ let destroyed=false;
957
+
958
+ function visibleError(error,fallback){
959
+ const message=typeof error?.message==='string'
960
+ ?error.message.trim()
961
+ :'';
962
+ return (message||fallback).slice(0,240);
963
+ }
964
+
965
+ function cancellation(error){
966
+ return error?.name==='AbortError'
967
+ ||['ARCANE_AI_REQUEST_ABORTED','AI_CHAT_ABORTED'].includes(error?.code);
968
+ }
969
+
970
+ function selected(){
971
+ return typeof role?.providerId==='string'
972
+ &&role.providerId.length>0
973
+ &&typeof role?.modelId==='string'
974
+ &&role.modelId.length>0;
975
+ }
976
+
977
+ function progressMessage(){
978
+ if(!role?.progress){
979
+ return 'The selected language model is loading.';
980
+ }
981
+ const progress=role.progress;
982
+ const amount=progress.total===null
983
+ ?`${progress.completed} ${progress.unit}`
984
+ :`${progress.completed} of ${progress.total} ${progress.unit}`;
985
+ return `${progress.phase}, ${amount}${progress.heartbeat?', active heartbeat':''}.`;
986
+ }
987
+
988
+ function render(){
989
+ const visible=selected()&&role.state!=='ready';
990
+ panel.hidden=!visible;
991
+ panel.setAttribute(
992
+ 'aria-busy',
993
+ String(requestPending||['loading','unloading'].includes(role?.state))
994
+ );
995
+ if(!visible){
996
+ return;
997
+ }
998
+ if(requestPending){
999
+ title.textContent='Requesting language model activation';
1000
+ status.textContent='Waiting for the selected language model runtime.';
1001
+ button.textContent='Requesting…';
1002
+ button.disabled=true;
1003
+ return;
1004
+ }
1005
+ if(role.state==='loading'){
1006
+ title.textContent='Starting language model';
1007
+ status.textContent=progressMessage();
1008
+ button.textContent='Cancel loading';
1009
+ button.disabled=false;
1010
+ return;
1011
+ }
1012
+ if(role.state==='unloading'){
1013
+ title.textContent='Canceling language model load';
1014
+ status.textContent='Waiting for the selected language model runtime to stop.';
1015
+ button.textContent='Canceling…';
1016
+ button.disabled=true;
1017
+ return;
1018
+ }
1019
+ if(role.state==='error'||requestError){
1020
+ title.textContent='Language model activation failed';
1021
+ status.textContent=requestError||visibleError(
1022
+ role.error,
1023
+ 'The selected language model could not start.'
1024
+ );
1025
+ button.textContent='Try again';
1026
+ button.disabled=false;
1027
+ return;
1028
+ }
1029
+ if(role.state==='unloaded'){
1030
+ title.textContent='Language model not active';
1031
+ status.textContent='Start the selected language model to enable chat.';
1032
+ button.textContent='Start language model';
1033
+ button.disabled=false;
1034
+ return;
1035
+ }
1036
+ title.textContent='Language model unavailable';
1037
+ status.textContent='The selected language model cannot be activated in this state.';
1038
+ button.textContent='Unavailable';
1039
+ button.disabled=true;
1040
+ }
1041
+
1042
+ async function request(action){
1043
+ if(destroyed||requestPending||!selected()){
1044
+ return false;
1045
+ }
1046
+ const permitted=action==='load'
1047
+ ?['unloaded','error'].includes(role.state)
1048
+ :action==='unload'&&role.state==='loading';
1049
+ if(!permitted){
1050
+ return false;
1051
+ }
1052
+ requestError='';
1053
+ const intent=Object.freeze({role:'llm',action,reason:'user'});
1054
+ const request=Object.freeze({intent,state:role});
1055
+ const activationEvent=new EventClass(
1056
+ 'chat-ai-activation-request',
1057
+ {
1058
+ bubbles:true,
1059
+ composed:true,
1060
+ cancelable:true,
1061
+ detail:request
1062
+ }
1063
+ );
1064
+ if(!host.dispatchEvent(activationEvent)){
1065
+ render();
1066
+ return false;
1067
+ }
1068
+ requestPending=true;
1069
+ render();
1070
+ try{
1071
+ await host.requestAIActivation(intent);
1072
+ return true;
1073
+ }catch(error){
1074
+ if(action==='load'
1075
+ &&cancellation(error)
1076
+ &&['unloaded','unloading'].includes(role?.state)){
1077
+ return false;
1078
+ }
1079
+ const message=visibleError(
1080
+ error,
1081
+ `The language model ${action} request failed.`
1082
+ );
1083
+ requestError=message;
1084
+ host.dispatchEvent(
1085
+ new EventClass(
1086
+ 'chat-ai-activation-error',
1087
+ {
1088
+ bubbles:true,
1089
+ composed:true,
1090
+ detail:Object.freeze({request,error,message})
1091
+ }
1092
+ )
1093
+ );
1094
+ return false;
1095
+ }finally{
1096
+ requestPending=false;
1097
+ render();
1098
+ }
1099
+ }
1100
+
1101
+ function activateSelectedAI(){
1102
+ const action=role?.state==='loading'?'unload':'load';
1103
+ void request(action);
1104
+ }
1105
+
1106
+ function synchronize(nextRole){
1107
+ if(destroyed){
1108
+ return;
1109
+ }
1110
+ if(role?.state!==nextRole.state
1111
+ ||role?.providerId!==nextRole.providerId
1112
+ ||role?.modelId!==nextRole.modelId){
1113
+ requestError='';
1114
+ requestPending=false;
1115
+ }
1116
+ role=nextRole;
1117
+ render();
1118
+ }
1119
+
1120
+ function destroy(){
1121
+ if(destroyed){
1122
+ return;
1123
+ }
1124
+ destroyed=true;
1125
+ button.removeEventListener('click',activateSelectedAI);
1126
+ panel.hidden=true;
1127
+ }
1128
+
1129
+ button.addEventListener('click',activateSelectedAI);
1130
+ return Object.freeze({request,synchronize,destroy});
1131
+ }
1132
+
864
1133
  function synchronizeAIRuntimeState(snapshot){
1134
+ aiActivationController.synchronize(snapshot.roles.llm);
865
1135
  applyAIAvailability({
866
1136
  llm:snapshot.roles.llm.state==='ready',
867
1137
  stt:snapshot.roles.stt.state==='ready',
@@ -1432,6 +1702,7 @@
1432
1702
 
1433
1703
  function destroy(){
1434
1704
  aiRuntimeStateAbortController.abort();
1705
+ aiActivationController.destroy();
1435
1706
  window.removeEventListener('pagehide',destroy);
1436
1707
  speech.destroy?.();
1437
1708
  host.ready=false;