1+ import { builtInAI } from "@built-in-ai/core" ;
12import { Textarea , UnstyledButton } from "@mantine/core" ;
23import { useDisclosure } from "@mantine/hooks" ;
34import { type ModelMessage , streamText } from "ai" ;
@@ -25,11 +26,17 @@ import SettingsModal from "./components/SettingsModal";
2526import TextCompletionView from "./components/TextCompletionView" ;
2627import type { ConversationSnapshot } from "./tree/types" ;
2728import { useConversationTree } from "./tree/useConversationTree" ;
28- import type { AppView , ModelInfo } from "./types" ;
29+ import type {
30+ AppView ,
31+ BuiltInAvailability ,
32+ ModelInfo ,
33+ ProviderKind ,
34+ } from "./types" ;
2935
3036const baseURLKey = "iaslate_baseURL" ;
3137const apiKeyKey = "iaslate_apiKey" ;
3238const modelsKey = "iaslate_models" ;
39+ const providerKindKey = "iaslate_provider_kind" ;
3340
3441const defaultSystemPrompt = "You are a helpful assistant." ;
3542
@@ -38,6 +45,10 @@ const App = () => {
3845 const [ apiKey , setAPIKey ] = useState ( "" ) ;
3946 const [ models , setModels ] = useImmer < ModelInfo [ ] > ( [ ] ) ;
4047 const [ activeModel , setActiveModel ] = useImmer < string | null > ( null ) ;
48+ const [ providerKind , setProviderKind ] =
49+ useState < ProviderKind > ( "openai-compatible" ) ;
50+ const [ builtInAvailability , setBuiltInAvailability ] =
51+ useState < BuiltInAvailability > ( "unknown" ) ;
4152
4253 const openAIProvider = useMemo (
4354 ( ) =>
@@ -48,16 +59,54 @@ const App = () => {
4859 [ apiKey , baseURL ] ,
4960 ) ;
5061
62+ const builtInStatusText = useMemo ( ( ) => {
63+ if ( providerKind !== "built-in" ) {
64+ return undefined ;
65+ }
66+ switch ( builtInAvailability ) {
67+ case "downloading" :
68+ return "Built-in AI downloading..." ;
69+ case "available" :
70+ return "Built-in AI ready" ;
71+ case "downloadable" :
72+ return "Download model in Settings" ;
73+ case "unavailable" :
74+ return "Built-in AI unavailable" ;
75+ default :
76+ return "Built-in AI" ;
77+ }
78+ } , [ builtInAvailability , providerKind ] ) ;
79+
80+ const getBuiltInChatModel = useCallback ( ( ) => {
81+ // Return a fresh model so each send can create a new session with the latest system prompt.
82+ return builtInAI ( ) ;
83+ } , [ ] ) ;
84+
85+ const refreshBuiltInAvailability = useCallback ( async ( ) => {
86+ try {
87+ const availability = await builtInAI ( ) . availability ( ) ;
88+ setBuiltInAvailability ( availability as BuiltInAvailability ) ;
89+ } catch ( error ) {
90+ console . error ( error ) ;
91+ setBuiltInAvailability ( "unavailable" ) ;
92+ }
93+ } , [ ] ) ;
94+
5195 const syncModels = useCallback (
5296 async ( {
5397 baseURLOverride,
5498 apiKeyOverride,
5599 silent = false ,
100+ force = false ,
56101 } : {
57102 baseURLOverride ?: string ;
58103 apiKeyOverride ?: string ;
59104 silent ?: boolean ;
105+ force ?: boolean ;
60106 } = { } ) => {
107+ if ( ! force && providerKind !== "openai-compatible" ) {
108+ return [ ] ;
109+ }
61110 const targetBaseURL = ( baseURLOverride ?? baseURL ) . trim ( ) ;
62111 const targetAPIKey = apiKeyOverride ?? apiKey ;
63112
@@ -97,11 +146,15 @@ const App = () => {
97146 return [ ] ;
98147 }
99148 } ,
100- [ activeModel , apiKey , baseURL , setActiveModel , setModels ] ,
149+ [ activeModel , apiKey , baseURL , providerKind , setActiveModel , setModels ] ,
101150 ) ;
102151
103152 useEffect ( ( ) => {
104153 ( async ( ) => {
154+ const storedProvider = await get < ProviderKind > ( providerKindKey ) ;
155+ if ( storedProvider ) {
156+ setProviderKind ( storedProvider ) ;
157+ }
105158 const storedBaseURL = await get < string > ( baseURLKey ) ;
106159 if ( storedBaseURL ) {
107160 setBaseURL ( storedBaseURL ) ;
@@ -129,6 +182,16 @@ const App = () => {
129182 } ) ( ) ;
130183 } , [ setActiveModel , setModels , syncModels ] ) ;
131184
185+ useEffect ( ( ) => {
186+ void refreshBuiltInAvailability ( ) ;
187+ } , [ refreshBuiltInAvailability ] ) ;
188+
189+ useEffect ( ( ) => {
190+ if ( providerKind === "built-in" ) {
191+ setActiveModel ( null ) ;
192+ }
193+ } , [ providerKind , setActiveModel ] ) ;
194+
132195 const [ isGenerating , setIsGenerating ] = useState ( false ) ;
133196 const [ prompt , setPrompt ] = useImmer ( "" ) ;
134197 const [ textContent , setTextContent ] = useImmer ( "" ) ;
@@ -241,6 +304,12 @@ const App = () => {
241304 } ;
242305 } , [ view ] ) ;
243306
307+ useEffect ( ( ) => {
308+ if ( view === "text" && providerKind === "built-in" ) {
309+ toast . error ( "Built-in AI supports chat only" ) ;
310+ }
311+ } , [ providerKind , view ] ) ;
312+
244313 const streamControllersRef = useRef < Record < string , AbortController > > ( { } ) ;
245314 const latestAssistantIdRef = useRef < string | undefined > ( undefined ) ;
246315 const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
@@ -377,6 +446,10 @@ const App = () => {
377446 if ( isTextGenerating ) {
378447 return ;
379448 }
449+ if ( providerKind !== "openai-compatible" ) {
450+ toast . error ( "Built-in AI supports chat only" ) ;
451+ return ;
452+ }
380453 if ( ! activeModel ) {
381454 toast . error ( "Select a model before predicting" ) ;
382455 return ;
@@ -390,7 +463,7 @@ const App = () => {
390463 setIsTextGenerating ( true ) ;
391464 try {
392465 const stream = streamText ( {
393- model : openAIProvider . completionModel ( activeModel ) ,
466+ model : openAIProvider ! . completionModel ( activeModel ! ) ,
394467 prompt : textContent ,
395468 temperature : 0.3 ,
396469 abortSignal : abortController . signal ,
@@ -426,14 +499,21 @@ const App = () => {
426499 } ;
427500
428501 const handleSend = async ( ) => {
429- if ( ! activeModel ) {
430- toast . error ( "Select a model before sending" ) ;
431- return ;
432- }
433- if ( ! openAIProvider ) {
434- toast . error ( "Set an API base URL before sending" ) ;
502+ const usingOpenAI = providerKind === "openai-compatible" ;
503+ if ( usingOpenAI ) {
504+ if ( ! activeModel ) {
505+ toast . error ( "Select a model before sending" ) ;
506+ return ;
507+ }
508+ if ( ! openAIProvider ) {
509+ toast . error ( "Set an API base URL before sending" ) ;
510+ return ;
511+ }
512+ } else if ( builtInAvailability !== "available" ) {
513+ toast . error ( "Download the built-in model in Settings before chatting" ) ;
435514 return ;
436515 }
516+ const builtInModel = usingOpenAI ? null : getBuiltInChatModel ( ) ;
437517 const trimmedPrompt = prompt . trim ( ) ;
438518 let resolvedParentId = activeTail ( ) ?? activeTargetId ;
439519 if ( ! resolvedParentId ) {
@@ -457,7 +537,9 @@ const App = () => {
457537 } ) ) ;
458538 try {
459539 const stream = streamText ( {
460- model : openAIProvider . chatModel ( activeModel ) ,
540+ model : usingOpenAI
541+ ? openAIProvider ! . chatModel ( activeModel ! )
542+ : builtInModel ! ,
461543 messages : contextMessages ,
462544 temperature : 0.3 ,
463545 abortSignal : abortController . signal ,
@@ -530,19 +612,28 @@ const App = () => {
530612 const handleSettingsSave = async ( {
531613 baseURL : nextBaseURL ,
532614 apiKey : nextAPIKey ,
615+ providerKind : nextProviderKind ,
533616 } : {
534617 baseURL : string ;
535618 apiKey : string ;
619+ providerKind : ProviderKind ;
536620 } ) => {
621+ setProviderKind ( nextProviderKind ) ;
622+ await set ( providerKindKey , nextProviderKind ) ;
537623 setBaseURL ( nextBaseURL ) ;
538624 await set ( baseURLKey , nextBaseURL ) ;
539625 setAPIKey ( nextAPIKey ) ;
540626 await set ( apiKeyKey , nextAPIKey ) ;
541- await syncModels ( {
542- baseURLOverride : nextBaseURL ,
543- apiKeyOverride : nextAPIKey ,
544- silent : true ,
545- } ) ;
627+ if ( nextProviderKind === "openai-compatible" ) {
628+ await syncModels ( {
629+ baseURLOverride : nextBaseURL ,
630+ apiKeyOverride : nextAPIKey ,
631+ silent : true ,
632+ force : true ,
633+ } ) ;
634+ } else {
635+ setActiveModel ( null ) ;
636+ }
546637 onSettingsClose ( ) ;
547638 } ;
548639
@@ -560,6 +651,13 @@ const App = () => {
560651 models = { models }
561652 activeModel = { activeModel }
562653 onModelChange = { setActiveModel }
654+ modelSelectorDisabled = { providerKind !== "openai-compatible" }
655+ modelPlaceholder = {
656+ providerKind === "openai-compatible"
657+ ? "Select a model"
658+ : "Built-in AI (no model list)"
659+ }
660+ modelStatus = { builtInStatusText }
563661 view = { view }
564662 onViewChange = { setView }
565663 onClear = { handleClearConversation }
@@ -685,6 +783,12 @@ const App = () => {
685783 < TextCompletionView
686784 value = { textContent }
687785 isGenerating = { isTextGenerating }
786+ isPredictDisabled = { providerKind !== "openai-compatible" }
787+ disabledReason = {
788+ providerKind !== "openai-compatible"
789+ ? "Built-in AI supports chat only"
790+ : undefined
791+ }
688792 onChange = { ( value ) => {
689793 setTextContent ( value ) ;
690794 } }
@@ -696,6 +800,9 @@ const App = () => {
696800 open = { isSettingsOpen }
697801 baseURL = { baseURL }
698802 apiKey = { apiKey }
803+ providerKind = { providerKind }
804+ builtInAvailability = { builtInAvailability }
805+ onBuiltInAvailabilityChange = { setBuiltInAvailability }
699806 onClose = { onSettingsClose }
700807 onSave = { handleSettingsSave }
701808 onSyncModels = { handleSyncModels }
0 commit comments