Core CI / core (ubuntu-latest) (push) Failing after 3m6s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m41s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
87 lines
2.2 KiB
TypeScript
87 lines
2.2 KiB
TypeScript
/** Optional generation is separate from local search and never starts itself. */
|
|
export interface AiConfiguration {
|
|
enabled: boolean;
|
|
base_url: string;
|
|
model: string;
|
|
api_key: string | null;
|
|
}
|
|
const bytes = (text: string) => new TextEncoder().encode(text).length;
|
|
export function aiConfigurationProblem(config: AiConfiguration): string | null {
|
|
if (!config.enabled) return "Optional AI is disabled.";
|
|
let endpoint: URL;
|
|
try {
|
|
endpoint = new URL(config.base_url);
|
|
} catch {
|
|
return "Enter your OpenAI-compatible endpoint URL.";
|
|
}
|
|
if (
|
|
!["http:", "https:"].includes(endpoint.protocol) ||
|
|
endpoint.username ||
|
|
endpoint.password ||
|
|
endpoint.search ||
|
|
endpoint.hash ||
|
|
bytes(config.base_url) > 2048
|
|
)
|
|
return "Use an HTTP(S) endpoint without URL credentials, query or fragment.";
|
|
if (
|
|
!config.model.trim() ||
|
|
bytes(config.model) > 200 ||
|
|
/[\u0000-\u001f\u007f]/u.test(config.model)
|
|
)
|
|
return "Enter a model name (up to 200 bytes).";
|
|
if (
|
|
config.api_key &&
|
|
(bytes(config.api_key) > 2048 ||
|
|
/[\u0000-\u001f\u007f]/u.test(config.api_key))
|
|
)
|
|
return "The session API key is invalid or too long.";
|
|
return null;
|
|
}
|
|
export function aiQuestionProblem(question: string): string | null {
|
|
return !question.trim()
|
|
? "Enter a question about this passage."
|
|
: bytes(question) > 2048
|
|
? "The question exceeds 2048 bytes."
|
|
: null;
|
|
}
|
|
export function aiEndpointLabel(baseURL: string) {
|
|
try {
|
|
return new URL(baseURL).origin;
|
|
} catch {
|
|
return "your configured endpoint";
|
|
}
|
|
}
|
|
export function aiSourceEligible(
|
|
source?: { kind: string; number: number },
|
|
media = false,
|
|
) {
|
|
return (
|
|
!media &&
|
|
Boolean(
|
|
source &&
|
|
["page", "paragraph", "line"].includes(source.kind) &&
|
|
Number.isInteger(source.number) &&
|
|
source.number > 0,
|
|
)
|
|
);
|
|
}
|
|
export class AiRevision {
|
|
private revision = 0;
|
|
private selection = "";
|
|
invalidate() {
|
|
this.revision++;
|
|
}
|
|
select(selection: string) {
|
|
if (selection !== this.selection) {
|
|
this.selection = selection;
|
|
this.invalidate();
|
|
}
|
|
}
|
|
current() {
|
|
return this.revision;
|
|
}
|
|
accepts(revision: number) {
|
|
return revision === this.revision;
|
|
}
|
|
}
|