Skip to content

Ejemplos ​

Dos aplicaciones de referencia que viven en el repositorio de ur, en apps/examples/. Cada una está construida sobre @nykk/ur-agent-client y ningún otro paquete de ur; sus únicas dependencias extra son marked y dompurify, para convertir en HTML —y sanear— el markdown que devuelve el agente. Ambas usan createChatStore: la diferencia entre ellas es deliberadamente solo de estilo, para mostrar que el SDK no impone nada en ese eje.

Si tienes acceso al repositorio, puedes arrancarlas con los comandos de cada sección. Si no, las dos piezas que de verdad importan están reproducidas enteras aquí abajo y se copian tal cual a tu proyecto.

Antes de arrancar cualquiera de los dos ​

Necesitas el id (o el identifier) de un agente real con su canal de SDK habilitado. En el panel: ficha del agente → Canales → «Integración a medida con tu propia interfaz» (@nykk/ur-agent-client); internamente es el canal js_sdk. Se crea activado junto con el agente, así que normalmente no hay que tocar nada (ver el quickstart si no tienes un agente a mano).

Pega esos dos valores en el módulo de config del proyecto que vayas a probar — Angular en src/app/agent-chat.config.ts, React en src/app/agent-chat-config.ts:

ts
export const AGENT_CHAT_CONFIG = {
  agent: '<id-del-agente>',
  // `undefined` = mismo origen que esta página, que es lo normal en desarrollo
  // (el proxy de `/embed` reenvía al web-server local).
  apiBase: undefined as string | undefined,
};

Angular ​

apps/examples/angular-chat — Tailwind + DaisyUI (mismo look que el panel de ur, con las clases chat/chat-bubble que DaisyUI ya trae pensadas para esto).

bash
# desde el repositorio de ur — en tu propio proyecto este script no existe
pnpm start:example-angular-chat   # http://localhost:4201

El serve del ejemplo depende de web-server:serve, que a su vez levanta el docker-compose.dev.yml: el comando arranca también Postgres, Redis y el backend en http://localhost:3000, que es adonde el ejemplo proxea /embed.

Todo el "pegamento" con el SDK vive en agent-chat.service.ts: crea el UrAgentClient con la config de arriba, lo envuelve con createChatStore, y refleja store.getState() en un signal dentro de store.subscribe(...) — el resto de la app solo lee ese signal.

ts
@Injectable({ providedIn: 'root' })
export class AgentChatService implements OnDestroy {
  private readonly client = new UrAgentClient({
    agent: AGENT_CHAT_CONFIG.agent,
    apiBase: AGENT_CHAT_CONFIG.apiBase,
  });
  private readonly store = createChatStore(this.client);
  private readonly unsubscribe: () => void;

  readonly state = signal<UrAgentChatState>(this.store.getState());

  constructor() {
    this.unsubscribe = this.store.subscribe(() => this.state.set(this.store.getState()));
    void this.restore();
  }

  private async restore(): Promise<void> {
    // Un subject nuevo no tiene hilo previo — nada que restaurar.
    if (!this.client.isNewSubject) {
      await this.store.loadHistory();
    }
    // El chat siempre está a la vista: si sigue vacío, el agente abre con su bienvenida.
    await this.store.start();
  }

  send(text: string): void {
    void this.store.send(text);
  }

  reset(): void {
    this.store.reset();
    void this.store.start();
  }

  ngOnDestroy(): void {
    this.unsubscribe();
    this.client.abort();
  }
}

app.ts solo inyecta este servicio, lee chat.state() en el template y llama a chat.send(...) / chat.reset() — no conoce el streaming ni el protocolo por debajo.

React ​

apps/examples/react-chat — CSS plano a mano, sin ningún framework de estilos, a propósito, como contraste con el ejemplo Angular.

bash
# desde el repositorio de ur — en tu propio proyecto este script no existe
pnpm start:example-react-chat   # http://localhost:4202

Mismo patrón que el servicio Angular, encapsulado en el hook useUrAgentChat, pero el store se conecta a React vía useSyncExternalStore(store.subscribe, store.getState) — sin adaptador intermedio, es exactamente la forma que ese hook ya espera:

ts
export function useUrAgentChat() {
  // Lazy initializer: se ejecuta una sola vez por instancia montada del hook.
  const [{ client, store }] = useState(() => {
    const client = new UrAgentClient({
      agent: AGENT_CHAT_CONFIG.agent,
      apiBase: AGENT_CHAT_CONFIG.apiBase,
    });
    return { client, store: createChatStore(client) };
  });

  const state = useSyncExternalStore(store.subscribe, store.getState);

  useEffect(() => {
    void (async () => {
      if (!client.isNewSubject) {
        await store.loadHistory();
      }
      // El chat siempre está a la vista: si sigue vacío, el agente abre con su bienvenida.
      await store.start();
    })();
    return () => client.abort();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const send = useCallback((text: string) => void store.send(text), [store]);
  const reset = useCallback(() => {
    store.reset();
    void store.start();
  }, [store]);

  return { state, send, reset } as const;
}

app.tsx + chat.css son el render: leen state y llaman a send/reset, igual que en Angular. Salvando el estilo, esta capa de adaptación al framework es lo único que cambia de un ejemplo a otro.

Los dos pintan la respuesta del agente como markdown saneado; si vas a hacer lo mismo, mira markdown-inline.pipe.ts (Angular) y render-markdown.ts (React).

Otros frameworks ​

El mismo patrón para Vue, Svelte y vanilla está en Integrar con un agente de IA.