@ulam/halohalo 0.3.2 → 0.3.3
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.
- package/README.md +212 -212
- package/agenticAiService.js +76 -76
- package/aiService.js +31 -31
- package/angular.js +102 -102
- package/connectivity.js +17 -17
- package/createCompletion.js +113 -113
- package/createProviderConfig.js +85 -85
- package/fetch.js +134 -134
- package/index.js +20 -20
- package/init.js +33 -33
- package/models.js +40 -48
- package/package.json +1 -1
- package/react.js +2 -2
- package/useCompletion.js +17 -17
- package/useProviderConfig.js +27 -27
- package/vue.js +73 -73
package/angular.js
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @ulam/halohalo/angular: Angular adapter
|
|
3
|
-
*
|
|
4
|
-
* Injectable services wrapping the vanilla halohalo core with Angular signal
|
|
5
|
-
* reactivity. Provide once at the application root via provideHalohalo().
|
|
6
|
-
*/
|
|
7
|
-
import { Injectable, signal, computed } from '@angular/core'
|
|
8
|
-
import { createCompletion } from './createCompletion.js'
|
|
9
|
-
import { createProviderConfig } from './createProviderConfig.js'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Service for AI completions. Each inject() call gets its own completion
|
|
13
|
-
* instance; loading/animating state is per-instance.
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* private completion = inject(CompletionService)
|
|
17
|
-
*
|
|
18
|
-
* await this.completion.complete({ prompt: 'Rewrite for mobile.' })
|
|
19
|
-
* // this.completion.loading() is true while running
|
|
20
|
-
*/
|
|
21
|
-
@Injectable()
|
|
22
|
-
export class CompletionService {
|
|
23
|
-
#instance = createCompletion()
|
|
24
|
-
#loading = signal(false)
|
|
25
|
-
#animating = signal(false)
|
|
26
|
-
|
|
27
|
-
loading = computed(() => this.#loading())
|
|
28
|
-
animating = computed(() => this.#animating())
|
|
29
|
-
|
|
30
|
-
constructor() {
|
|
31
|
-
this.#instance.subscribe(({ loading, animating }) => {
|
|
32
|
-
this.#loading.set(loading)
|
|
33
|
-
this.#animating.set(animating)
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
complete(options) {
|
|
38
|
-
return this.#instance.complete(options)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
cancel() {
|
|
42
|
-
this.#instance.cancel()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
ngOnDestroy() {
|
|
46
|
-
this.#instance.cancel()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Service for AI provider and model configuration. One config store per application.
|
|
52
|
-
*
|
|
53
|
-
* Usage:
|
|
54
|
-
* private providerConfig = inject(ProviderConfigService)
|
|
55
|
-
*
|
|
56
|
-
* this.providerConfig.setProvider('openai')
|
|
57
|
-
* const current = this.providerConfig.provider() // Signal<string>
|
|
58
|
-
*/
|
|
59
|
-
@Injectable({ providedIn: 'root' })
|
|
60
|
-
export class ProviderConfigService {
|
|
61
|
-
#config = createProviderConfig()
|
|
62
|
-
#provider = signal(this.#config.provider)
|
|
63
|
-
#models = signal(this.#config.models)
|
|
64
|
-
#mode = signal(this.#config.mode)
|
|
65
|
-
|
|
66
|
-
provider = computed(() => this.#provider())
|
|
67
|
-
models = computed(() => this.#models())
|
|
68
|
-
mode = computed(() => this.#mode())
|
|
69
|
-
providers = this.#config.providers
|
|
70
|
-
|
|
71
|
-
constructor() {
|
|
72
|
-
this.#config.subscribe(() => {
|
|
73
|
-
this.#provider.set(this.#config.provider)
|
|
74
|
-
this.#models.set(this.#config.models)
|
|
75
|
-
this.#mode.set(this.#config.mode)
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
setProvider(id) { this.#config.setProvider(id) }
|
|
80
|
-
setModel(pid, mid) { this.#config.setModel(pid, mid) }
|
|
81
|
-
setMode(v) { this.#config.setMode(v) }
|
|
82
|
-
setKey(pid, v) { return this.#config.setKey(pid, v) }
|
|
83
|
-
getKey(pid) { return this.#config.getKey(pid) }
|
|
84
|
-
getModel(pid) { return this.#config.getModel(pid) }
|
|
85
|
-
getLabel(pid) { return this.#config.getLabel(pid) }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Standalone provider function. Use in bootstrapApplication() or as a
|
|
90
|
-
* component-level provider when you need scoped completion instances.
|
|
91
|
-
*
|
|
92
|
-
* App-level (shared singleton):
|
|
93
|
-
* bootstrapApplication(AppComponent, {
|
|
94
|
-
* providers: [provideHalohalo()]
|
|
95
|
-
* })
|
|
96
|
-
*
|
|
97
|
-
* Component-level (scoped instance):
|
|
98
|
-
* @Component({ providers: [CompletionService] })
|
|
99
|
-
*/
|
|
100
|
-
export function provideHalohalo() {
|
|
101
|
-
return [ProviderConfigService, CompletionService]
|
|
102
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @ulam/halohalo/angular: Angular adapter
|
|
3
|
+
*
|
|
4
|
+
* Injectable services wrapping the vanilla halohalo core with Angular signal
|
|
5
|
+
* reactivity. Provide once at the application root via provideHalohalo().
|
|
6
|
+
*/
|
|
7
|
+
import { Injectable, signal, computed } from '@angular/core'
|
|
8
|
+
import { createCompletion } from './createCompletion.js'
|
|
9
|
+
import { createProviderConfig } from './createProviderConfig.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Service for AI completions. Each inject() call gets its own completion
|
|
13
|
+
* instance; loading/animating state is per-instance.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* private completion = inject(CompletionService)
|
|
17
|
+
*
|
|
18
|
+
* await this.completion.complete({ prompt: 'Rewrite for mobile.' })
|
|
19
|
+
* // this.completion.loading() is true while running
|
|
20
|
+
*/
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class CompletionService {
|
|
23
|
+
#instance = createCompletion()
|
|
24
|
+
#loading = signal(false)
|
|
25
|
+
#animating = signal(false)
|
|
26
|
+
|
|
27
|
+
loading = computed(() => this.#loading())
|
|
28
|
+
animating = computed(() => this.#animating())
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.#instance.subscribe(({ loading, animating }) => {
|
|
32
|
+
this.#loading.set(loading)
|
|
33
|
+
this.#animating.set(animating)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
complete(options) {
|
|
38
|
+
return this.#instance.complete(options)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
cancel() {
|
|
42
|
+
this.#instance.cancel()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ngOnDestroy() {
|
|
46
|
+
this.#instance.cancel()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Service for AI provider and model configuration. One config store per application.
|
|
52
|
+
*
|
|
53
|
+
* Usage:
|
|
54
|
+
* private providerConfig = inject(ProviderConfigService)
|
|
55
|
+
*
|
|
56
|
+
* this.providerConfig.setProvider('openai')
|
|
57
|
+
* const current = this.providerConfig.provider() // Signal<string>
|
|
58
|
+
*/
|
|
59
|
+
@Injectable({ providedIn: 'root' })
|
|
60
|
+
export class ProviderConfigService {
|
|
61
|
+
#config = createProviderConfig()
|
|
62
|
+
#provider = signal(this.#config.provider)
|
|
63
|
+
#models = signal(this.#config.models)
|
|
64
|
+
#mode = signal(this.#config.mode)
|
|
65
|
+
|
|
66
|
+
provider = computed(() => this.#provider())
|
|
67
|
+
models = computed(() => this.#models())
|
|
68
|
+
mode = computed(() => this.#mode())
|
|
69
|
+
providers = this.#config.providers
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
this.#config.subscribe(() => {
|
|
73
|
+
this.#provider.set(this.#config.provider)
|
|
74
|
+
this.#models.set(this.#config.models)
|
|
75
|
+
this.#mode.set(this.#config.mode)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setProvider(id) { this.#config.setProvider(id) }
|
|
80
|
+
setModel(pid, mid) { this.#config.setModel(pid, mid) }
|
|
81
|
+
setMode(v) { this.#config.setMode(v) }
|
|
82
|
+
setKey(pid, v) { return this.#config.setKey(pid, v) }
|
|
83
|
+
getKey(pid) { return this.#config.getKey(pid) }
|
|
84
|
+
getModel(pid) { return this.#config.getModel(pid) }
|
|
85
|
+
getLabel(pid) { return this.#config.getLabel(pid) }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Standalone provider function. Use in bootstrapApplication() or as a
|
|
90
|
+
* component-level provider when you need scoped completion instances.
|
|
91
|
+
*
|
|
92
|
+
* App-level (shared singleton):
|
|
93
|
+
* bootstrapApplication(AppComponent, {
|
|
94
|
+
* providers: [provideHalohalo()]
|
|
95
|
+
* })
|
|
96
|
+
*
|
|
97
|
+
* Component-level (scoped instance):
|
|
98
|
+
* @Component({ providers: [CompletionService] })
|
|
99
|
+
*/
|
|
100
|
+
export function provideHalohalo() {
|
|
101
|
+
return [ProviderConfigService, CompletionService]
|
|
102
|
+
}
|
package/connectivity.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Probes a list of endpoints in parallel to check network reachability.
|
|
3
|
-
*
|
|
4
|
-
* @param {Array<{ label: string, url: string }>} probes
|
|
5
|
-
* @param {number} [timeoutMs=4000]
|
|
6
|
-
* @returns {Promise<Array<{ label: string, ok: boolean }>>}
|
|
7
|
-
*/
|
|
8
|
-
export async function checkConnectivity(probes, timeoutMs = 4000) {
|
|
9
|
-
const results = await Promise.allSettled(
|
|
10
|
-
probes.map(p =>
|
|
11
|
-
fetch(p.url, { method: 'HEAD', mode: 'no-cors', signal: AbortSignal.timeout(timeoutMs) })
|
|
12
|
-
.then(() => ({ label: p.label, ok: true }))
|
|
13
|
-
.catch(() => ({ label: p.label, ok: false }))
|
|
14
|
-
)
|
|
15
|
-
)
|
|
16
|
-
return results.map(r => r.value || r.reason)
|
|
17
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Probes a list of endpoints in parallel to check network reachability.
|
|
3
|
+
*
|
|
4
|
+
* @param {Array<{ label: string, url: string }>} probes
|
|
5
|
+
* @param {number} [timeoutMs=4000]
|
|
6
|
+
* @returns {Promise<Array<{ label: string, ok: boolean }>>}
|
|
7
|
+
*/
|
|
8
|
+
export async function checkConnectivity(probes, timeoutMs = 4000) {
|
|
9
|
+
const results = await Promise.allSettled(
|
|
10
|
+
probes.map(p =>
|
|
11
|
+
fetch(p.url, { method: 'HEAD', mode: 'no-cors', signal: AbortSignal.timeout(timeoutMs) })
|
|
12
|
+
.then(() => ({ label: p.label, ok: true }))
|
|
13
|
+
.catch(() => ({ label: p.label, ok: false }))
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
return results.map(r => r.value || r.reason)
|
|
17
|
+
}
|
package/createCompletion.js
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import { callProvider, callAnthropicWithTools } from './fetch.js'
|
|
2
|
-
import { AiApiError } from './providers.js'
|
|
3
|
-
|
|
4
|
-
const DEFAULT_TYPEWRITER = { tickMs: 33, minCharsPerTick: 2, charDivisor: 40 }
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Vanilla completion runner. No React required.
|
|
8
|
-
* Returns { complete(options), cancel() } plus a subscribe() for state changes.
|
|
9
|
-
*
|
|
10
|
-
* State: { loading: boolean, animating: boolean }
|
|
11
|
-
*
|
|
12
|
-
* options for complete():
|
|
13
|
-
* provider, model, key, prompt, maxTokens: for standard completions
|
|
14
|
-
* agentOptions: { system, tools, messages, maxTurns, onToolCall }: for agentic
|
|
15
|
-
* onResult(fields) : called with parsed result after completion
|
|
16
|
-
* onError(error) : called with AiApiError on failure
|
|
17
|
-
* parseResponse(text) : maps raw text to result fields object
|
|
18
|
-
* typewriter : { tickMs, minCharsPerTick, charDivisor } or false
|
|
19
|
-
* onAnimate(field, text) : called each tick with field + current text slice
|
|
20
|
-
* onAnimateDone() : called when typewriter finishes
|
|
21
|
-
*/
|
|
22
|
-
export function createCompletion() {
|
|
23
|
-
let loading = false
|
|
24
|
-
let animating = false
|
|
25
|
-
let timer = null
|
|
26
|
-
const listeners = new Set()
|
|
27
|
-
const notify = () => listeners.forEach(fn => fn({ loading, animating }))
|
|
28
|
-
|
|
29
|
-
function runTypewriter(fields, { typewriter, onAnimate, onAnimateDone }) {
|
|
30
|
-
clearTimeout(timer)
|
|
31
|
-
const { tickMs, minCharsPerTick, charDivisor } = { ...DEFAULT_TYPEWRITER, ...typewriter }
|
|
32
|
-
|
|
33
|
-
const entries = Object.entries(fields).filter(([, v]) => typeof v === 'string' && v.length > 0)
|
|
34
|
-
if (!entries.length) {
|
|
35
|
-
animating = false
|
|
36
|
-
notify()
|
|
37
|
-
onAnimateDone?.()
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const total = entries.reduce((sum, [, v]) => sum + v.length, 0)
|
|
42
|
-
const charsPerTick = Math.max(minCharsPerTick, Math.ceil(total / charDivisor))
|
|
43
|
-
const indices = Object.fromEntries(entries.map(([k]) => [k, 0]))
|
|
44
|
-
let entryIdx = 0
|
|
45
|
-
|
|
46
|
-
function tick() {
|
|
47
|
-
const [field, text] = entries[entryIdx]
|
|
48
|
-
indices[field] = Math.min(indices[field] + charsPerTick, text.length)
|
|
49
|
-
onAnimate?.(field, text.slice(0, indices[field]))
|
|
50
|
-
if (indices[field] >= text.length) entryIdx++
|
|
51
|
-
if (entryIdx >= entries.length) {
|
|
52
|
-
animating = false
|
|
53
|
-
notify()
|
|
54
|
-
onAnimateDone?.()
|
|
55
|
-
} else {
|
|
56
|
-
timer = setTimeout(tick, tickMs)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
timer = setTimeout(tick, tickMs)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
get loading() { return loading },
|
|
65
|
-
get animating() { return animating },
|
|
66
|
-
|
|
67
|
-
subscribe(fn) {
|
|
68
|
-
listeners.add(fn)
|
|
69
|
-
return () => listeners.delete(fn)
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
async complete({ provider, model, key, prompt, maxTokens, agentOptions, onResult, onError, parseResponse, typewriter = DEFAULT_TYPEWRITER, onAnimate, onAnimateDone }) {
|
|
73
|
-
loading = true
|
|
74
|
-
notify()
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
let text
|
|
78
|
-
if (agentOptions) {
|
|
79
|
-
const { system, tools, messages, maxTurns, onToolCall } = agentOptions
|
|
80
|
-
text = await callAnthropicWithTools({ key, model, system, tools, messages, maxTokens, maxTurns, onToolCall })
|
|
81
|
-
} else {
|
|
82
|
-
text = await callProvider({ provider, model, key, prompt, maxTokens })
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const result = parseResponse ? parseResponse(text) : { text }
|
|
86
|
-
loading = false
|
|
87
|
-
|
|
88
|
-
if (typewriter && onAnimate) {
|
|
89
|
-
animating = true
|
|
90
|
-
notify()
|
|
91
|
-
onResult?.(result)
|
|
92
|
-
runTypewriter(result, { typewriter, onAnimate, onAnimateDone })
|
|
93
|
-
} else {
|
|
94
|
-
notify()
|
|
95
|
-
onResult?.(result)
|
|
96
|
-
onAnimateDone?.()
|
|
97
|
-
}
|
|
98
|
-
} catch (e) {
|
|
99
|
-
loading = false
|
|
100
|
-
animating = false
|
|
101
|
-
notify()
|
|
102
|
-
onError?.(e instanceof AiApiError ? e : new AiApiError('api_error'))
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
cancel() {
|
|
107
|
-
clearTimeout(timer)
|
|
108
|
-
loading = false
|
|
109
|
-
animating = false
|
|
110
|
-
notify()
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
import { callProvider, callAnthropicWithTools } from './fetch.js'
|
|
2
|
+
import { AiApiError } from './providers.js'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TYPEWRITER = { tickMs: 33, minCharsPerTick: 2, charDivisor: 40 }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vanilla completion runner. No React required.
|
|
8
|
+
* Returns { complete(options), cancel() } plus a subscribe() for state changes.
|
|
9
|
+
*
|
|
10
|
+
* State: { loading: boolean, animating: boolean }
|
|
11
|
+
*
|
|
12
|
+
* options for complete():
|
|
13
|
+
* provider, model, key, prompt, maxTokens: for standard completions
|
|
14
|
+
* agentOptions: { system, tools, messages, maxTurns, onToolCall }: for agentic
|
|
15
|
+
* onResult(fields) : called with parsed result after completion
|
|
16
|
+
* onError(error) : called with AiApiError on failure
|
|
17
|
+
* parseResponse(text) : maps raw text to result fields object
|
|
18
|
+
* typewriter : { tickMs, minCharsPerTick, charDivisor } or false
|
|
19
|
+
* onAnimate(field, text) : called each tick with field + current text slice
|
|
20
|
+
* onAnimateDone() : called when typewriter finishes
|
|
21
|
+
*/
|
|
22
|
+
export function createCompletion() {
|
|
23
|
+
let loading = false
|
|
24
|
+
let animating = false
|
|
25
|
+
let timer = null
|
|
26
|
+
const listeners = new Set()
|
|
27
|
+
const notify = () => listeners.forEach(fn => fn({ loading, animating }))
|
|
28
|
+
|
|
29
|
+
function runTypewriter(fields, { typewriter, onAnimate, onAnimateDone }) {
|
|
30
|
+
clearTimeout(timer)
|
|
31
|
+
const { tickMs, minCharsPerTick, charDivisor } = { ...DEFAULT_TYPEWRITER, ...typewriter }
|
|
32
|
+
|
|
33
|
+
const entries = Object.entries(fields).filter(([, v]) => typeof v === 'string' && v.length > 0)
|
|
34
|
+
if (!entries.length) {
|
|
35
|
+
animating = false
|
|
36
|
+
notify()
|
|
37
|
+
onAnimateDone?.()
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const total = entries.reduce((sum, [, v]) => sum + v.length, 0)
|
|
42
|
+
const charsPerTick = Math.max(minCharsPerTick, Math.ceil(total / charDivisor))
|
|
43
|
+
const indices = Object.fromEntries(entries.map(([k]) => [k, 0]))
|
|
44
|
+
let entryIdx = 0
|
|
45
|
+
|
|
46
|
+
function tick() {
|
|
47
|
+
const [field, text] = entries[entryIdx]
|
|
48
|
+
indices[field] = Math.min(indices[field] + charsPerTick, text.length)
|
|
49
|
+
onAnimate?.(field, text.slice(0, indices[field]))
|
|
50
|
+
if (indices[field] >= text.length) entryIdx++
|
|
51
|
+
if (entryIdx >= entries.length) {
|
|
52
|
+
animating = false
|
|
53
|
+
notify()
|
|
54
|
+
onAnimateDone?.()
|
|
55
|
+
} else {
|
|
56
|
+
timer = setTimeout(tick, tickMs)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
timer = setTimeout(tick, tickMs)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
get loading() { return loading },
|
|
65
|
+
get animating() { return animating },
|
|
66
|
+
|
|
67
|
+
subscribe(fn) {
|
|
68
|
+
listeners.add(fn)
|
|
69
|
+
return () => listeners.delete(fn)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async complete({ provider, model, key, prompt, maxTokens, agentOptions, onResult, onError, parseResponse, typewriter = DEFAULT_TYPEWRITER, onAnimate, onAnimateDone }) {
|
|
73
|
+
loading = true
|
|
74
|
+
notify()
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
let text
|
|
78
|
+
if (agentOptions) {
|
|
79
|
+
const { system, tools, messages, maxTurns, onToolCall } = agentOptions
|
|
80
|
+
text = await callAnthropicWithTools({ key, model, system, tools, messages, maxTokens, maxTurns, onToolCall })
|
|
81
|
+
} else {
|
|
82
|
+
text = await callProvider({ provider, model, key, prompt, maxTokens })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = parseResponse ? parseResponse(text) : { text }
|
|
86
|
+
loading = false
|
|
87
|
+
|
|
88
|
+
if (typewriter && onAnimate) {
|
|
89
|
+
animating = true
|
|
90
|
+
notify()
|
|
91
|
+
onResult?.(result)
|
|
92
|
+
runTypewriter(result, { typewriter, onAnimate, onAnimateDone })
|
|
93
|
+
} else {
|
|
94
|
+
notify()
|
|
95
|
+
onResult?.(result)
|
|
96
|
+
onAnimateDone?.()
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
loading = false
|
|
100
|
+
animating = false
|
|
101
|
+
notify()
|
|
102
|
+
onError?.(e instanceof AiApiError ? e : new AiApiError('api_error'))
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
cancel() {
|
|
107
|
+
clearTimeout(timer)
|
|
108
|
+
loading = false
|
|
109
|
+
animating = false
|
|
110
|
+
notify()
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
package/createProviderConfig.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { DEFAULT_MODELS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_LABELS } from './providers.js'
|
|
2
|
-
|
|
3
|
-
async function getAdapter() {
|
|
4
|
-
const mod = await import('@ulam/sawsawan')
|
|
5
|
-
return mod.getAdapter()
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Vanilla provider config store. No React required.
|
|
10
|
-
* Returns a plain object with getters, setters, and a subscribe() for change notifications.
|
|
11
|
-
*
|
|
12
|
-
* storageKeys: { provider, modelPrefix, keyPrefix, mode? }
|
|
13
|
-
* providers: optional array of { id, label, defaultModel? }
|
|
14
|
-
*/
|
|
15
|
-
export async function createProviderConfig(storageKeys, providers = DEFAULT_PROVIDERS) {
|
|
16
|
-
const { provider: providerKey, modelPrefix, keyPrefix, mode: modeKey } = storageKeys
|
|
17
|
-
const adapter = await getAdapter()
|
|
18
|
-
|
|
19
|
-
const providerList = providers.map(p =>
|
|
20
|
-
typeof p === 'string' ? { id: p, label: DEFAULT_PROVIDER_LABELS[p] || p } : p
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
let provider = adapter.readPref(providerKey) || providerList[0]?.id || 'anthropic'
|
|
24
|
-
|
|
25
|
-
let models = Object.fromEntries(
|
|
26
|
-
providerList.map(p => [
|
|
27
|
-
p.id,
|
|
28
|
-
adapter.readPref(`${modelPrefix}${p.id}`) || p.defaultModel || DEFAULT_MODELS[p.id] || '',
|
|
29
|
-
])
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
let mode = modeKey ? adapter.readPref(modeKey) === 'true' : false
|
|
33
|
-
|
|
34
|
-
const listeners = new Set()
|
|
35
|
-
const notify = () => listeners.forEach(fn => fn())
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
get provider() { return provider },
|
|
39
|
-
get models() { return { ...models } },
|
|
40
|
-
get mode() { return mode },
|
|
41
|
-
get providers() { return providerList },
|
|
42
|
-
|
|
43
|
-
setProvider(id) {
|
|
44
|
-
adapter.writePref(providerKey, id)
|
|
45
|
-
provider = id
|
|
46
|
-
notify()
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
setModel(providerId, modelId) {
|
|
50
|
-
adapter.writePref(`${modelPrefix}${providerId}`, modelId)
|
|
51
|
-
models = { ...models, [providerId]: modelId }
|
|
52
|
-
notify()
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
setMode(value) {
|
|
56
|
-
if (!modeKey) return
|
|
57
|
-
adapter.writePref(modeKey, value ? 'true' : 'false')
|
|
58
|
-
mode = value
|
|
59
|
-
notify()
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
async setKey(providerId, value) {
|
|
63
|
-
await adapter.setKey(`${keyPrefix}${providerId}`, value)
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
async getKey(providerId) {
|
|
67
|
-
return (await adapter.getKey(`${keyPrefix}${providerId}`)) || ''
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
getModel(providerId) {
|
|
71
|
-
return models[providerId] || DEFAULT_MODELS[providerId] || ''
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
getLabel(providerId) {
|
|
75
|
-
return providerList.find(p => p.id === providerId)?.label
|
|
76
|
-
|| DEFAULT_PROVIDER_LABELS[providerId]
|
|
77
|
-
|| providerId
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
subscribe(fn) {
|
|
81
|
-
listeners.add(fn)
|
|
82
|
-
return () => listeners.delete(fn)
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
import { DEFAULT_MODELS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_LABELS } from './providers.js'
|
|
2
|
+
|
|
3
|
+
async function getAdapter() {
|
|
4
|
+
const mod = await import('@ulam/sawsawan')
|
|
5
|
+
return mod.getAdapter()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Vanilla provider config store. No React required.
|
|
10
|
+
* Returns a plain object with getters, setters, and a subscribe() for change notifications.
|
|
11
|
+
*
|
|
12
|
+
* storageKeys: { provider, modelPrefix, keyPrefix, mode? }
|
|
13
|
+
* providers: optional array of { id, label, defaultModel? }
|
|
14
|
+
*/
|
|
15
|
+
export async function createProviderConfig(storageKeys, providers = DEFAULT_PROVIDERS) {
|
|
16
|
+
const { provider: providerKey, modelPrefix, keyPrefix, mode: modeKey } = storageKeys
|
|
17
|
+
const adapter = await getAdapter()
|
|
18
|
+
|
|
19
|
+
const providerList = providers.map(p =>
|
|
20
|
+
typeof p === 'string' ? { id: p, label: DEFAULT_PROVIDER_LABELS[p] || p } : p
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
let provider = adapter.readPref(providerKey) || providerList[0]?.id || 'anthropic'
|
|
24
|
+
|
|
25
|
+
let models = Object.fromEntries(
|
|
26
|
+
providerList.map(p => [
|
|
27
|
+
p.id,
|
|
28
|
+
adapter.readPref(`${modelPrefix}${p.id}`) || p.defaultModel || DEFAULT_MODELS[p.id] || '',
|
|
29
|
+
])
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
let mode = modeKey ? adapter.readPref(modeKey) === 'true' : false
|
|
33
|
+
|
|
34
|
+
const listeners = new Set()
|
|
35
|
+
const notify = () => listeners.forEach(fn => fn())
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
get provider() { return provider },
|
|
39
|
+
get models() { return { ...models } },
|
|
40
|
+
get mode() { return mode },
|
|
41
|
+
get providers() { return providerList },
|
|
42
|
+
|
|
43
|
+
setProvider(id) {
|
|
44
|
+
adapter.writePref(providerKey, id)
|
|
45
|
+
provider = id
|
|
46
|
+
notify()
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
setModel(providerId, modelId) {
|
|
50
|
+
adapter.writePref(`${modelPrefix}${providerId}`, modelId)
|
|
51
|
+
models = { ...models, [providerId]: modelId }
|
|
52
|
+
notify()
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
setMode(value) {
|
|
56
|
+
if (!modeKey) return
|
|
57
|
+
adapter.writePref(modeKey, value ? 'true' : 'false')
|
|
58
|
+
mode = value
|
|
59
|
+
notify()
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async setKey(providerId, value) {
|
|
63
|
+
await adapter.setKey(`${keyPrefix}${providerId}`, value)
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async getKey(providerId) {
|
|
67
|
+
return (await adapter.getKey(`${keyPrefix}${providerId}`)) || ''
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
getModel(providerId) {
|
|
71
|
+
return models[providerId] || DEFAULT_MODELS[providerId] || ''
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getLabel(providerId) {
|
|
75
|
+
return providerList.find(p => p.id === providerId)?.label
|
|
76
|
+
|| DEFAULT_PROVIDER_LABELS[providerId]
|
|
77
|
+
|| providerId
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
subscribe(fn) {
|
|
81
|
+
listeners.add(fn)
|
|
82
|
+
return () => listeners.delete(fn)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|