UNPKG

@hashbrownai/angular

Version:
1,305 lines (1,274 loc) 57.3 kB
import * as i0 from '@angular/core'; import { inject, Injector, runInInjectionContext, untracked, isSignal, computed, reflectComponentType, DestroyRef, signal, input, ApplicationRef, ViewContainerRef, Component, Directive, output, contentChild, ViewEncapsulation, ChangeDetectionStrategy, InjectionToken, effect } from '@angular/core'; import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { ɵcreateRuntimeFunctionImpl as _createRuntimeFunctionImpl, ɵcreateRuntimeImpl as _createRuntimeImpl, s, prepareMagicText, fryHashbrown, ɵui as _ui } from '@hashbrownai/core'; /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Creates a function with an input schema. * * @public * @param cfg - The configuration for the function containing: * - `name`: The name of the function * - `description`: The description of the function * - `args`: The args schema of the function * - `result`: The result schema of the function * - `handler`: The handler of the function * @returns The function reference. */ function createRuntimeFunction(cfg) { const injector = inject(Injector); return _createRuntimeFunctionImpl({ ...cfg, handler: (...args) => { return runInInjectionContext(injector, () => cfg.handler(...args)); }, }); } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Creates a new runtime. * * @public * @param options - The options for creating the runtime. * @returns A reference to the runtime. */ function createRuntime(options) { return _createRuntimeImpl(options); } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * @public */ function createTool(input) { if ('schema' in input) { const { name, description, schema, handler } = input; return { name, description, schema, handler: (args, abortSignal) => handler(args, abortSignal), }; } else { const { name, description, handler } = input; return { name, description, schema: s.object('Empty Object', {}), handler: (_, abortSignal) => handler(abortSignal), }; } } function bindToolToInjector(tool, injector) { return { ...tool, handler: (args, abortSignal) => untracked(() => runInInjectionContext(injector, () => tool.handler(args, abortSignal))), }; } /** * The schema for the javascript tool. */ const schema = s.streaming.object('The result', { code: s.streaming.string('The JavaScript code to run'), }); /** * Creates a tool that allows the LLM to run JavaScript code. It is run * in a stateful JavaScript environment, with no access to the internet, the DOM, * or any function that you have not explicitly defined. * * @public * @param options - The options for creating the tool. * @returns The tool. */ function createToolJavaScript({ runtime, }) { return createTool({ name: 'javascript', description: [ 'Whenever you send a message containing JavaScript code to javascript, it will be', 'executed in a stateful JavaScript environment. javascript will respond with the output', 'of the execution or time out after ${runtime.timeout / 1000} seconds. Internet access', 'for this session is disabled. Do not make external web requests or API calls as they', 'will fail.', '', 'Important: Prefer calling javascript once with a large amount of code, rather than calling it', 'multiple times with smaller amounts of code.', '', 'The following functions are available to you:', runtime.describe(), ].join('\n'), schema, handler: async ({ code }, abortSignal) => { return runtime.run(code, abortSignal); }, }); } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Symbol used to mark signals that were created by toDeepSignal. * This helps us identify and clean up stale deep signal properties * when the structure of the data changes. */ const DEEP_SIGNAL = Symbol('DEEP_SIGNAL'); /** * Converts a Signal to a DeepSignal, allowing reactive access to nested properties. * * This implementation is lifted from @ngrx/signals and uses a Proxy to lazily create * computed signals for nested properties as they are accessed. * * @param signal - The signal to convert to a deep signal * @returns A DeepSignal that allows accessing nested properties as signals * * @remarks * The implementation uses a Proxy to intercept property access and lazily creates * computed signals for nested properties. This ensures: * - Minimal memory overhead (signals are only created when accessed) * - Automatic cleanup of stale signals when data structure changes * - Full reactivity for nested properties * * @example * ```typescript * const messages = signal([{ content: { text: 'Hello' } }]); * const deepMessages = toDeepSignal(messages); * * // In a component or effect: * effect(() => { * // This will re-run when the text changes * console.log(deepMessages()[0].content.text); * }); * ``` * * @public */ function toDeepSignal(signal) { return new Proxy(signal, { has(target, prop) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return !!this.get(target, prop, undefined); }, get(target, prop) { const value = untracked(target); if (!isRecord(value) || !(prop in value)) { if (isSignal(target[prop]) && target[prop][DEEP_SIGNAL]) { delete target[prop]; } return target[prop]; } if (!isSignal(target[prop])) { Object.defineProperty(target, prop, { value: computed(() => target()[prop]), configurable: true, }); target[prop][DEEP_SIGNAL] = true; } return toDeepSignal(target[prop]); }, }); } const nonRecords = [ WeakSet, WeakMap, Promise, Date, Error, RegExp, ArrayBuffer, DataView, Function, ]; function isRecord(value) { if (value === null || typeof value !== 'object' || isIterable(value)) { return false; } let proto = Object.getPrototypeOf(value); if (proto === Object.prototype) { return true; } while (proto && proto !== Object.prototype) { if (nonRecords.includes(proto.constructor)) { return false; } proto = Object.getPrototypeOf(proto); } return proto === Object.prototype; } function isIterable(value) { return typeof value?.[Symbol.iterator] === 'function'; } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Exposes a component by combining it with additional configuration details. * * @public * @typeParam T - The type of the Angular component. * @param component - The Angular component to be exposed. * @param config - The configuration object for the component, excluding the component itself. * @returns An object representing the exposed component, including the component and its configuration. */ function exposeComponent(component, config) { const reflected = reflectComponentType(component); if (!reflected?.selector) { throw new Error(`Could not reflect component: ${component}`); } const { input, name, ...rest } = config; return { component, ...rest, props: input, name: name ?? reflected?.selector, }; } function readSignalLike(signalLike) { if (isSignal(signalLike)) { return signalLike(); } if (typeof signalLike === 'function') { return signalLike(); } return signalLike; } function toNgSignal(source, debugName) { const destroyRef = inject(DestroyRef); const options = debugName ? { debugName } : undefined; const _signal = signal(source(), options); const teardown = source.subscribe((value) => { _signal.set(value); }); destroyRef.onDestroy(() => { teardown(); }); return _signal.asReadonly(); } const TAG_NAME_REGISTRY = Symbol('ɵtagNameRegistry'); const getTagNameRegistry = (message) => { if (TAG_NAME_REGISTRY in message) { return message[TAG_NAME_REGISTRY]; } return undefined; }; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @angular-eslint/component-selector */ /** * Renders messages generated by the assistant from uiChatResource. * * @public * @example * * ```html * <hb-render-message [message]="message" /> * ``` */ class RenderMessageComponent { message = input.required(...(ngDevMode ? [{ debugName: "message" }] : [])); /** * @internal */ appRef = inject(ApplicationRef); /** * @internal */ content = computed(() => this.message().content?.ui ?? [], ...(ngDevMode ? [{ debugName: "content" }] : [])); /** * @internal */ tagNameRegistry = computed(() => getTagNameRegistry(this.message()), ...(ngDevMode ? [{ debugName: "tagNameRegistry" }] : [])); /** * @internal */ viewContainerRef = inject(ViewContainerRef); /** * @internal */ rootNodesWeakMap = new WeakMap(); /** * @internal */ embeddedViewsWeakMap = new WeakMap(); /** * @internal */ getTagComponent(tagName) { return this.tagNameRegistry()?.[tagName]?.component ?? null; } /** * @internal */ getEmbeddedView(tpl) { if (this.embeddedViewsWeakMap.has(tpl)) { return this.embeddedViewsWeakMap.get(tpl); } const view = this.viewContainerRef.createEmbeddedView(tpl); this.embeddedViewsWeakMap.set(tpl, view); return view; } /** * @internal */ getRootNodes(tpl) { if (this.rootNodesWeakMap.has(tpl)) { return this.rootNodesWeakMap.get(tpl); } const view = this.getEmbeddedView(tpl); const nodes = [view.rootNodes]; this.rootNodesWeakMap.set(tpl, nodes); return nodes; } /** * @internal */ isTextNode(node) { return typeof node.$children === 'string'; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: RenderMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: RenderMessageComponent, isStandalone: true, selector: "hb-render-message", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: ` <ng-template #nodeTemplateRef let-node="node"> <ng-template #childrenTemplateRef> @if (isTextNode(node)) { {{ node.$children }} } @else { @for (child of node.$children; track $index) { <ng-container *ngTemplateOutlet="nodeTemplateRef; context: { node: child }" /> } } </ng-template> @if (node) { <ng-container *ngComponentOutlet=" getTagComponent(node.$tag); inputs: node.$props; content: getRootNodes(childrenTemplateRef) " ></ng-container> } </ng-template> @if (content()) { @for (node of content(); track $index) { <ng-template [ngTemplateOutlet]="nodeTemplateRef" [ngTemplateOutletContext]="node" > </ng-template> <ng-container *ngTemplateOutlet="nodeTemplateRef; context: { node: node }" /> } } `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: RenderMessageComponent, decorators: [{ type: Component, args: [{ selector: 'hb-render-message', imports: [NgComponentOutlet, NgTemplateOutlet], template: ` <ng-template #nodeTemplateRef let-node="node"> <ng-template #childrenTemplateRef> @if (isTextNode(node)) { {{ node.$children }} } @else { @for (child of node.$children; track $index) { <ng-container *ngTemplateOutlet="nodeTemplateRef; context: { node: child }" /> } } </ng-template> @if (node) { <ng-container *ngComponentOutlet=" getTagComponent(node.$tag); inputs: node.$props; content: getRootNodes(childrenTemplateRef) " ></ng-container> } </ng-template> @if (content()) { @for (node of content(); track $index) { <ng-template [ngTemplateOutlet]="nodeTemplateRef" [ngTemplateOutletContext]="node" > </ng-template> <ng-container *ngTemplateOutlet="nodeTemplateRef; context: { node: node }" /> } } `, }] }] }); /* eslint-disable @angular-eslint/component-class-suffix */ /* eslint-disable no-useless-escape */ /* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/directive-selector */ class MagicTextRenderLink { template; constructor(template) { this.template = template; } static ngTemplateContextGuard(dir, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderLink, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.4", type: MagicTextRenderLink, isStandalone: true, selector: "ng-template[hbMagicTextRenderLink]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderLink, decorators: [{ type: Directive, args: [{ selector: 'ng-template[hbMagicTextRenderLink]' }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); class MagicTextRenderText { template; constructor(template) { this.template = template; } static ngTemplateContextGuard(dir, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderText, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.4", type: MagicTextRenderText, isStandalone: true, selector: "ng-template[hbMagicTextRenderText]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderText, decorators: [{ type: Directive, args: [{ selector: 'ng-template[hbMagicTextRenderText]' }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** @public */ class MagicTextRenderCitation { template; constructor(template) { this.template = template; } static ngTemplateContextGuard(dir, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderCitation, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.4", type: MagicTextRenderCitation, isStandalone: true, selector: "ng-template[hbMagicTextRenderCitation]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderCitation, decorators: [{ type: Directive, args: [{ selector: 'ng-template[hbMagicTextRenderCitation]' }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** @public */ class MagicTextRenderWhitespace { template; constructor(template) { this.template = template; } static ngTemplateContextGuard(dir, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderWhitespace, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.4", type: MagicTextRenderWhitespace, isStandalone: true, selector: "ng-template[hbMagicTextRenderWhitespace]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicTextRenderWhitespace, decorators: [{ type: Directive, args: [{ selector: 'ng-template[hbMagicTextRenderWhitespace]' }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** @public */ class MagicText { text = input.required(...(ngDevMode ? [{ debugName: "text" }] : [])); defaultLinkTarget = input('_blank', ...(ngDevMode ? [{ debugName: "defaultLinkTarget" }] : [])); defaultLinkRel = input('noopener noreferrer', ...(ngDevMode ? [{ debugName: "defaultLinkRel" }] : [])); citations = input(...(ngDevMode ? [undefined, { debugName: "citations" }] : [])); linkClick = output(); citationClick = output(); linkTemplate = contentChild(MagicTextRenderLink, ...(ngDevMode ? [{ debugName: "linkTemplate" }] : [])); textTemplate = contentChild(MagicTextRenderText, ...(ngDevMode ? [{ debugName: "textTemplate" }] : [])); citationTemplate = contentChild(MagicTextRenderCitation, ...(ngDevMode ? [{ debugName: "citationTemplate" }] : [])); whitespaceTemplate = contentChild(MagicTextRenderWhitespace, ...(ngDevMode ? [{ debugName: "whitespaceTemplate" }] : [])); fragments = computed(() => { const fragments = prepareMagicText(this.text()).fragments; return fragments.map((fragment, index, all) => { if (fragment.type !== 'text') { return fragment; } const next = all[index + 1]; const prev = all[index - 1]; // Keep natural spacing, but strip edge spaces that would create gaps // around tight footnote citations. let text = fragment.text.replace(/[\u00a0\u202f]/g, ' '); if (next?.type === 'citation') { text = text.replace(/\s+$/, ''); } if (prev?.type === 'citation') { text = text.replace(/^\s+/, ''); } return { ...fragment, text }; }); }, ...(ngDevMode ? [{ debugName: "fragments" }] : [])); citationLookup = computed(() => { const map = new Map(); for (const citation of this.citations() ?? []) { if (!citation) { continue; } const key = citation.id?.trim?.() ?? ''; const url = citation.url?.trim?.() ?? ''; if (key && url) { map.set(key, url); } } return map; }, ...(ngDevMode ? [{ debugName: "citationLookup" }] : [])); whitespaceContext(fragment, position, index) { const fragments = this.fragments(); const previous = fragments[index - 1]; let render = position === 'before' ? fragment.renderWhitespace.before : fragment.renderWhitespace.after; if (position === 'before' && index === 0) { render = false; } const next = fragments[index + 1]; const hasLeadingWhitespace = (frag) => frag?.type === 'text' && /^\s/.test(frag.text); const hasTrailingWhitespace = (frag) => frag?.type === 'text' && /\s$/.test(frag.text); if (position === 'before') { // If the surrounding fragments already carry whitespace in their text, // avoid rendering an extra spacer node. const hasWhitespaceInText = hasTrailingWhitespace(previous) || hasLeadingWhitespace(fragment); render = render && !hasWhitespaceInText; } if (position === 'after') { const hasWhitespaceInText = hasTrailingWhitespace(fragment) || hasLeadingWhitespace(next); render = render && !hasWhitespaceInText; } if (position === 'before' && fragment.type === 'citation') { render = false; } // Footnote-style citations should sit tight against the preceding text. if (position === 'after' && next?.type === 'citation') { render = false; } const startsTight = (frag) => frag?.type === 'text' && /^[,.;:!?|\)\]]/.test(frag.text.trim()); const endsWithNoGap = (frag) => frag?.type === 'text' && /([\(\|])$/.test(frag.text.trim()); if (position === 'before' && startsTight(fragment)) { render = false; } if (position === 'after' && startsTight(next)) { render = false; } if (position === 'after' && endsWithNoGap(fragment)) { render = false; } return { $implicit: { position, render, fragment }, position, render, fragment, index, }; } templateContext(node) { return { $implicit: node, node }; } toTextNode(fragment) { const text = this.normalizeFragmentText(fragment); return { text, tags: fragment.tags, state: fragment.state, isStatic: fragment.isStatic, renderWhitespace: fragment.renderWhitespace, isCode: fragment.isCode, fragment, }; } normalizeFragmentText(fragment) { // Normalize only non-breaking spaces; keep the original whitespace intact // so we don't double-insert gaps alongside rendered spacer nodes. return fragment.text.replace(/[\u00a0\u202f]/g, ' '); } toLinkNode(fragment) { const link = fragment.marks.link; if (!link) { throw new Error('Link fragment is missing link metadata.'); } return { ...this.toTextNode(fragment), href: link.href, title: link.title, ariaLabel: link.ariaLabel, rel: link.rel, target: link.target, link, }; } toCitationNode(fragment) { const url = this.citationLookup().get(String(fragment.citation.id)); const text = fragment.text.trim(); return { citation: { ...fragment.citation, url }, text, state: fragment.state, isStatic: fragment.isStatic, renderWhitespace: fragment.renderWhitespace, fragment, }; } handleLinkClick(event, fragment) { const href = fragment.marks.link?.href ?? ''; this.linkClick.emit({ mouseEvent: event, href, fragment }); } handleCitationClick(event, context) { this.citationClick.emit({ mouseEvent: event, citation: { id: context.citation.id, url: context.citation.url }, fragment: context.fragment, }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicText, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: MagicText, isStandalone: true, selector: "hb-magic-text", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, defaultLinkTarget: { classPropertyName: "defaultLinkTarget", publicName: "defaultLinkTarget", isSignal: true, isRequired: false, transformFunction: null }, defaultLinkRel: { classPropertyName: "defaultLinkRel", publicName: "defaultLinkRel", isSignal: true, isRequired: false, transformFunction: null }, citations: { classPropertyName: "citations", publicName: "citations", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { linkClick: "linkClick", citationClick: "citationClick" }, queries: [{ propertyName: "linkTemplate", first: true, predicate: MagicTextRenderLink, descendants: true, isSignal: true }, { propertyName: "textTemplate", first: true, predicate: MagicTextRenderText, descendants: true, isSignal: true }, { propertyName: "citationTemplate", first: true, predicate: MagicTextRenderCitation, descendants: true, isSignal: true }, { propertyName: "whitespaceTemplate", first: true, predicate: MagicTextRenderWhitespace, descendants: true, isSignal: true }], ngImport: i0, template: ` <ng-template #defaultWhitespace let-fragment="fragment" let-position="position" let-render="render" > @if (render) { <span class="hb-space" [class.hb-space--before]="position === 'before'" [class.hb-space--after]="position === 'after'" aria-hidden="true" >{{ ' ' }}</span > } </ng-template> <ng-template #defaultText let-node="node"> <span class="hb-text" [class.hb-text--code]="node.isCode" [class.hb-text--strong]="node.tags.includes('strong')" [class.hb-text--em]="node.tags.includes('em')" [attr.data-fragment-state]="node.state" animate.enter="hb-text--enter" >{{ node.text }}</span > </ng-template> <ng-template #defaultLink let-node="node"> <a class="hb-link" [attr.href]="node.href" [attr.title]="node.title || null" [attr.aria-label]="node.ariaLabel || null" [attr.rel]="node.rel || defaultLinkRel()" [attr.target]="node.target || defaultLinkTarget()" data-fragment-kind="text" [attr.data-fragment-state]="node.state" animate.enter="hb-text--enter" (click)="handleLinkClick($event, node.fragment)" > <ng-container *ngTemplateOutlet=" textTemplate()?.template ?? defaultText; context: templateContext(toTextNode(node.fragment)) " /> </a> </ng-template> <ng-template #defaultCitation let-node="node"> <span animate.enter="hb-text--enter"> @if (node.citation.url) { <a class="hb-citation" role="doc-noteref" [attr.href]="node.citation.url" [attr.rel]="defaultLinkRel()" [attr.target]="defaultLinkTarget()" data-fragment-kind="citation" [attr.data-fragment-state]="node.state" (click)="handleCitationClick($event, node)" >{{ node.text }}</a > } @else { <button type="button" class="hb-citation hb-citation-placeholder" role="doc-noteref" data-fragment-kind="citation" [attr.data-fragment-state]="node.state" (click)="handleCitationClick($event, node)" > {{ node.text }} </button> } </span> </ng-template> @for (fragment of fragments(); track fragment.key; let i = $index) { <ng-container *ngTemplateOutlet=" whitespaceTemplate()?.template ?? defaultWhitespace; context: whitespaceContext(fragment, 'before', i) " /> <span class="hb-fragment" [attr.data-fragment-kind]="fragment.type" [attr.data-fragment-state]="fragment.state" animate.enter="hb-text--enter" > @if (fragment.type === 'text') { @if (fragment.marks.link) { <ng-container *ngTemplateOutlet=" linkTemplate()?.template ?? defaultLink; context: templateContext(toLinkNode(fragment)) " /> } @else { <ng-container *ngTemplateOutlet=" textTemplate()?.template ?? defaultText; context: templateContext(toTextNode(fragment)) " /> } } @else { <ng-container *ngTemplateOutlet=" citationTemplate()?.template ?? defaultCitation; context: templateContext(toCitationNode(fragment)) " /> } </span> <ng-container *ngTemplateOutlet=" whitespaceTemplate()?.template ?? defaultWhitespace; context: whitespaceContext(fragment, 'after', i) " /> } `, isInline: true, styles: [".hb-text--code{font-family:monospace}.hb-text--strong{font-weight:700}.hb-text--em{font-style:italic}.hb-text--enter{animation:enter .35s ease-in-out}@keyframes enter{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: MagicText, decorators: [{ type: Component, args: [{ selector: 'hb-magic-text', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: ` <ng-template #defaultWhitespace let-fragment="fragment" let-position="position" let-render="render" > @if (render) { <span class="hb-space" [class.hb-space--before]="position === 'before'" [class.hb-space--after]="position === 'after'" aria-hidden="true" >{{ ' ' }}</span > } </ng-template> <ng-template #defaultText let-node="node"> <span class="hb-text" [class.hb-text--code]="node.isCode" [class.hb-text--strong]="node.tags.includes('strong')" [class.hb-text--em]="node.tags.includes('em')" [attr.data-fragment-state]="node.state" animate.enter="hb-text--enter" >{{ node.text }}</span > </ng-template> <ng-template #defaultLink let-node="node"> <a class="hb-link" [attr.href]="node.href" [attr.title]="node.title || null" [attr.aria-label]="node.ariaLabel || null" [attr.rel]="node.rel || defaultLinkRel()" [attr.target]="node.target || defaultLinkTarget()" data-fragment-kind="text" [attr.data-fragment-state]="node.state" animate.enter="hb-text--enter" (click)="handleLinkClick($event, node.fragment)" > <ng-container *ngTemplateOutlet=" textTemplate()?.template ?? defaultText; context: templateContext(toTextNode(node.fragment)) " /> </a> </ng-template> <ng-template #defaultCitation let-node="node"> <span animate.enter="hb-text--enter"> @if (node.citation.url) { <a class="hb-citation" role="doc-noteref" [attr.href]="node.citation.url" [attr.rel]="defaultLinkRel()" [attr.target]="defaultLinkTarget()" data-fragment-kind="citation" [attr.data-fragment-state]="node.state" (click)="handleCitationClick($event, node)" >{{ node.text }}</a > } @else { <button type="button" class="hb-citation hb-citation-placeholder" role="doc-noteref" data-fragment-kind="citation" [attr.data-fragment-state]="node.state" (click)="handleCitationClick($event, node)" > {{ node.text }} </button> } </span> </ng-template> @for (fragment of fragments(); track fragment.key; let i = $index) { <ng-container *ngTemplateOutlet=" whitespaceTemplate()?.template ?? defaultWhitespace; context: whitespaceContext(fragment, 'before', i) " /> <span class="hb-fragment" [attr.data-fragment-kind]="fragment.type" [attr.data-fragment-state]="fragment.state" animate.enter="hb-text--enter" > @if (fragment.type === 'text') { @if (fragment.marks.link) { <ng-container *ngTemplateOutlet=" linkTemplate()?.template ?? defaultLink; context: templateContext(toLinkNode(fragment)) " /> } @else { <ng-container *ngTemplateOutlet=" textTemplate()?.template ?? defaultText; context: templateContext(toTextNode(fragment)) " /> } } @else { <ng-container *ngTemplateOutlet=" citationTemplate()?.template ?? defaultCitation; context: templateContext(toCitationNode(fragment)) " /> } </span> <ng-container *ngTemplateOutlet=" whitespaceTemplate()?.template ?? defaultWhitespace; context: whitespaceContext(fragment, 'after', i) " /> } `, encapsulation: ViewEncapsulation.None, styles: [".hb-text--code{font-family:monospace}.hb-text--strong{font-weight:700}.hb-text--em{font-style:italic}.hb-text--enter{animation:enter .35s ease-in-out}@keyframes enter{0%{opacity:0}to{opacity:1}}\n"] }] }] }); /** * @internal */ const ɵHASHBROWN_CONFIG_INJECTION_TOKEN = new InjectionToken('HashbrownConfig'); /** * Provides the Hashbrown configuration. * * @public * @param options - The Hashbrown configuration. * @returns The Hashbrown configuration. */ function provideHashbrown(options) { return { provide: ɵHASHBROWN_CONFIG_INJECTION_TOKEN, useValue: options, }; } /** * @internal */ function ɵinjectHashbrownConfig() { return inject(ɵHASHBROWN_CONFIG_INJECTION_TOKEN); } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * This Angular resource provides a reactive chat interface for send and receiving messages from a model. * The resource-based API includes signals for the current messages, status, and control methods for sending and stopping messages. * * @public * @remarks * The `chatResource` function provides the most basic functionality for un-structured chats. Unstructured chats include things like general chats and natural language controls. * * @param options - Configuration for the chat resource. * @returns An object with reactive signals and methods for interacting with the chat. * @typeParam Tools - The set of tool definitions available to the chat. * @example * This example demonstrates how to use the `chatResource` function to create a simple chat component. * * ```ts * const chat = chatResource({ * system: 'hashbrowns should be covered and smothered', * model: 'gpt-5', * }); * * chat.sendMessage(\{ role: 'user', content: 'Write a short story about breakfast.' \}); * ``` */ function chatResource(options) { const config = ɵinjectHashbrownConfig(); const injector = inject(Injector); const destroyRef = inject(DestroyRef); const hashbrown = fryHashbrown({ apiUrl: options.apiUrl ?? config.baseUrl, middleware: config.middleware?.map((m) => { return (requestInit) => runInInjectionContext(injector, () => m(requestInit)); }), system: readSignalLike(options.system), model: readSignalLike(options.model), tools: options.tools?.map((tool) => bindToolToInjector(tool, injector)), emulateStructuredOutput: config.emulateStructuredOutput, debugName: options.debugName, transport: options.transport ?? config.transport, ui: false, threadId: readSignalLike(options.threadId), }); const teardown = hashbrown.sizzle(); destroyRef.onDestroy(() => teardown()); const value = toNgSignal(hashbrown.messages, options.debugName && `${options.debugName}.value`); const isReceiving = toNgSignal(hashbrown.isReceiving, options.debugName && `${options.debugName}.isReceiving`); const isSending = toNgSignal(hashbrown.isSending, options.debugName && `${options.debugName}.isSending`); const isGenerating = toNgSignal(hashbrown.isGenerating, options.debugName && `${options.debugName}.isGenerating`); const isRunningToolCalls = toNgSignal(hashbrown.isRunningToolCalls, options.debugName && `${options.debugName}.isRunningToolCalls`); const isLoading = toNgSignal(hashbrown.isLoading, options.debugName && `${options.debugName}.isLoading`); const error = toNgSignal(hashbrown.error, options.debugName && `${options.debugName}.error`); const sendingError = toNgSignal(hashbrown.sendingError, options.debugName && `${options.debugName}.sendingError`); const generatingError = toNgSignal(hashbrown.generatingError, options.debugName && `${options.debugName}.generatingError`); const lastAssistantMessage = toNgSignal(hashbrown.lastAssistantMessage, options.debugName && `${options.debugName}.lastAssistantMessage`); const isLoadingThread = toNgSignal(hashbrown.isLoadingThread, options.debugName && `${options.debugName}.isLoadingThread`); const isSavingThread = toNgSignal(hashbrown.isSavingThread, options.debugName && `${options.debugName}.isSavingThread`); const threadLoadError = toNgSignal(hashbrown.threadLoadError, options.debugName && `${options.debugName}.threadLoadError`); const threadSaveError = toNgSignal(hashbrown.threadSaveError, options.debugName && `${options.debugName}.threadSaveError`); const status = computed(() => { if (isLoading()) { return 'loading'; } if (error()) { return 'error'; } const hasAssistantMessage = value().some((message) => message.role === 'assistant'); if (hasAssistantMessage) { return 'resolved'; } return 'idle'; }, { debugName: options.debugName && `${options.debugName}.status` }); function reload() { const lastMessage = value()[value().length - 1]; if (lastMessage.role === 'assistant') { hashbrown.setMessages(value().slice(0, -1)); return true; } return false; } function hasValue() { return value().some((message) => message.role === 'assistant'); } function sendMessage(message) { hashbrown.sendMessage(message); } function stop(clearStreamingMessage = false) { hashbrown.stop(clearStreamingMessage); } return { hasValue: hasValue, status, isReceiving, isSending, isGenerating, isRunningToolCalls, isLoading, isLoadingThread, isSavingThread, sendingError, generatingError, threadLoadError, threadSaveError, reload, sendMessage, stop, value, error, lastAssistantMessage, }; } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Creates a completion resource. * * @public * @param options - The options for the completion resource. * @typeParam Input - The type of the input to the completion. * @returns The completion resource. */ function completionResource(options) { const { model, input, system } = options; const injector = inject(Injector); const destroyRef = inject(DestroyRef); const config = ɵinjectHashbrownConfig(); const hashbrown = fryHashbrown({ debugName: options.debugName, apiUrl: options.apiUrl ?? config.baseUrl, middleware: config.middleware?.map((m) => { return (requestInit) => runInInjectionContext(injector, () => m(requestInit)); }), model: readSignalLike(model), system: readSignalLike(system), messages: [], tools: [], retries: 3, transport: options.transport ?? config.transport, threadId: options.threadId ? readSignalLike(options.threadId) : undefined, }); const teardown = hashbrown.sizzle(); destroyRef.onDestroy(() => teardown()); const messages = toNgSignal(hashbrown.messages); const isReceiving = toNgSignal(hashbrown.isReceiving); const isSending = toNgSignal(hashbrown.isSending); const isGenerating = toNgSignal(hashbrown.isGenerating); const isRunningToolCalls = toNgSignal(hashbrown.isRunningToolCalls); const isLoading = toNgSignal(hashbrown.isLoading); const isLoadingThread = toNgSignal(hashbrown.isLoadingThread); const isSavingThread = toNgSignal(hashbrown.isSavingThread); const sendingError = toNgSignal(hashbrown.sendingError); const generatingError = toNgSignal(hashbrown.generatingError); const threadLoadError = toNgSignal(hashbrown.threadLoadError); const threadSaveError = toNgSignal(hashbrown.threadSaveError); const internalMessages = computed(() => { const _input = input(); if (!_input) { return []; } return [ { role: 'user', content: _input, }, ]; }, ...(ngDevMode ? [{ debugName: "internalMessages" }] : [])); const error = toNgSignal(hashbrown.error, options.debugName && `${options.debugName}.error`); const exhaustedRetries = toNgSignal(hashbrown.exhaustedRetries, options.debugName && `${options.debugName}.exhaustedRetries`); effect(() => { const _messages = internalMessages(); hashbrown.setMessages(_messages); }); const value = computed(() => { const lastMessage = messages()[messages().length - 1]; if (lastMessage && lastMessage.role === 'assistant' && lastMessage.content && typeof lastMessage.content === 'string') { return lastMessage.content; } return null; }, { debugName: options.debugName && `${options.debugName}.value` }); const status = computed(() => { if (isLoading()) { return 'loading'; } if (exhaustedRetries()) { return 'error'; } return 'idle'; }, ...(ngDevMode ? [{ debugName: "status" }] : [])); const reload = () => { return true; }; function hasValue() { return Boolean(value()); } function stop(clearStreamingMessage = false) { hashbrown.stop(clearStreamingMessage); } return { value, status, error, isLoading, isReceiving, isSending, isGenerating, isRunningToolCalls, isLoadingThread, isSavingThread, sendingError, generatingError, threadLoadError, threadSaveError, reload, stop, hasValue: hasValue, }; } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Creates a structured chat resource. * * @public * @param options - The options for the structured chat resource. * @returns The structured chat resource. */ function structuredChatResource(options) { const config = ɵinjectHashbrownConfig(); const injector = inject(Injector); const destroyRef = inject(DestroyRef); const hashbrown = fryHashbrown({ apiUrl: options.apiUrl ?? config.baseUrl, middleware: config.middleware?.map((m) => { return (requestInit) => runInInjectionContext(injector, () => m(requestInit)); }), system: readSignalLike(options.system), messages: [...(options.messages ?? [])], model: options.model, tools: options.tools?.map((tool) => bindToolToInjector(tool, injector)), responseSchema: options.schema, debugName: options.debugName, emulateStructuredOutput: config.emulateStructuredOutput, debounce: options.debounce, retries: options.retries, transport: options.transport ?? config.transport, ui: options.ui ?? false, threadId: options.threadId ? readSignalLike(options.threadId) : undefined, }); const optionsEffect = effect(() => { const model = options.model; const system = readSignalLike(options.system); const threadId = options.threadId ? readSignalLike(options.threadId) : undefined; hashbrown.updateOptions({ model, system, ui: options.ui ?? false, threadId, }); }, ...(ngDevMode ? [{ debugName: "optionsEffect" }] : [])); const teardown = hashbrown.sizzle(); destroyRef.onDestroy(() => { teardown(); optionsEffect.destroy(); }); const valueSignal = toNgSignal(hashbrown.messages, options.debugName && `${options.debugName}.value`); const value = toDeepSignal(valueSignal); const isReceiving = toNgSignal(hashbrown.isReceiving, options.debugName && `${options.debugName}.isReceiving`); const isSending = toNgSignal(hashbrown.isSending, options.debugName && `${options.debugName}.isSending`); const isGenerating = toNgSignal(hashbrown.isGenerating, options.debugName && `${options.debugName}.isGenerating`); const isRunningToolCalls = toNgSignal(hashbrown.isRunningToolCalls, options.debugName && `${options.debugName}.isRunningToolCalls`); const isLoading = toNgSignal(hashbrown.isLoading, options.debugName && `${options.debugName}.isLoading`); const error = toNgSignal(hashbrown.error, options.debugName && `${options.debugName}.error`); const sendingError = toNgSignal(hashbrown.sendingError, options.debugName && `${options.debugName}.sendingError`); const generatingError = toNgSignal(hashbrown.generatingError, options.debugName && `${options.debugName}.generatingError`); const lastAssistantMessage = toNgSignal(hashbrown.lastAssistantMessage, options.debugName && `${options.debugName}.lastAssistantMessage`); const exhaustedRetries = toNgSignal(hashbrown.exhaustedRetries); const isLoadingThread = toNgSignal(hashbrown.isLoadingThread, options.debugName && `${options.debugName}.isLoadingThread`); const isSavingThread = toNgSignal(hashbrown.isSavingThread, options.debugName && `${options.debugName}.isSavingThread`); const threadLoadError = toNgSignal(hashbrown.threadLoadError, options.debugName && `${options.debugName}.threadLoadError`); const threadSaveError = toNgSignal(hashbrown.threadSaveError, options.debugName && `${options.debugName}.threadSaveError`); const status = computed(() => { if (isLoading()) { return 'loading'; } if (exhaustedRetries()) { return 'error'; } const hasAssistantMessage = value().some((message) => message.role === 'assistant'); if (hasAssistantMessage) { return 'resolved'; } return 'idle'; }, { debugName: options.debugName && `${options.debugName}.status` }); function reload() { const lastMessage = value()[value().length - 1]; if (lastMessage.role === 'assistant') { hashbrown.setMessages(value().slice(0, -1)); return true; } return false; } function hasValue() { return value().some((message) => message.role === 'assistant'); } function sendMessage(message) { hashbrown.sendMessage(message); } function resendMessages() { hashbrown.resendMessages(); } function setMessages(messages) { hashbrown.setMessages(messages); } function stop(clearStreamingMessage = false) { hashbrown.stop(clearStreamingMessage); } return { hasValue: hasValue, status, isLoading, isGenerating, isSending, isReceiving, isRunningToolCalls, reload, sendMessage, resendMessages, stop, value, error, sendingError, generatingError, setMessages, lastAssistantMessage, isLoadingThread, isSavingThread, threadLoadError, threadSaveError, }; } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Creates a structured completion resource. * * @public * @param options - The options for the structured completion resource. * @returns The structured completion resource. */ function structuredCompletionResource(options) { const { model, input, schema, system, tools, debugName, apiUrl, retries, debounce, } = options; const resource = structuredChatResource({ model, system, schema, tools, debugName, apiUrl, retries, debounce, transport: options.transport, ui: options.ui ?? false, threadId: options.threadId, }); effect(() => { const _input = input(); if (!_input) { return; } resource.setMessages([ { role: 'user', content: _input, }, ]); }); const valueSignal = computed(() => { const lastMessage = resource.value()[resource.value().length - 1]; if (lastMessage