@trossino/chat-widget 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +116 -2
- package/dist/index.cjs +429 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +402 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +249 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +45 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.js +246 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +29 -0
- package/package.json +90 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/createChatHandler.ts"],"names":["parsed"],"mappings":";AAyBO,IAAM,cAAA,GAA+C;AAAA,EAC1D,SAAA,EAAW,mBAAA;AAAA,EACX,MAAA,EAAQ;AACV;AAEA,IAAM,aAAA,GAAgB,uCAAA;AACtB,IAAM,iBAAA,GAAoB,YAAA;AAC1B,IAAM,UAAA,GAAa,4CAAA;AAEnB,IAAM,aAAA,GAAgB,yCAAA;AAEtB,IAAM,aAAA,GAAN,cAA4B,KAAA,CAAM;AAAA,EAChC,WAAA,CACW,QAAA,EACA,MAAA,EACA,IAAA,EACT;AACA,IAAA,KAAA,CAAM,CAAA,EAAG,QAAQ,CAAA,2BAAA,EAA8B,MAAM,CAAA,CAAE,CAAA;AAJ9C,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAGT,IAAA,IAAA,CAAK,IAAA,GAAO,eAAA;AAAA,EACd;AAAA,EANW,QAAA;AAAA,EACA,MAAA;AAAA,EACA,IAAA;AAKb,CAAA;AAEO,SAAS,kBAAkB,OAAA,EAAgD;AAChF,EAAA,MAAM;AAAA,IACJ,QAAA;AAAA,IACA,MAAA;AAAA,IACA,KAAA,GAAQ,eAAe,QAAQ,CAAA;AAAA,IAC/B,YAAA;AAAA,IACA,SAAA,GAAY,IAAA;AAAA,IACZ,MAAA,GAAS,KAAA;AAAA,IACT,WAAA,GAAc,EAAA;AAAA,IACd,gBAAA,GAAmB,GAAA;AAAA,IACnB;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,IAAI,QAAA,KAAa,WAAA,IAAe,QAAA,KAAa,QAAA,EAAU;AACrD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,MAAA,CAAO,QAAQ,CAAC,CAAA,CAAE,CAAA;AAAA,EAC7D;AAEA,EAAA,OAAO,eAAe,YAAY,GAAA,EAAiC;AACjE,IAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,MAAA,OAAO,IAAA,CAAK,EAAE,KAAA,EAAO,oBAAA,IAAwB,GAAA,EAAK,EAAE,KAAA,EAAO,MAAA,EAAQ,CAAA;AAAA,IACrE;AAEA,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,OAAO,IAAA,CAAK,EAAE,KAAA,EAAO,gCAAA,IAAoC,GAAG,CAAA;AAAA,IAC9D;AAEA,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,IACxB,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA,CAAK,EAAE,KAAA,EAAO,mBAAA,IAAuB,GAAG,CAAA;AAAA,IACjD;AAEA,IAAA,MAAM,MAAA,GAAS,aAAA,CAAc,IAAA,EAAM,WAAA,EAAa,gBAAgB,CAAA;AAChE,IAAA,IAAI,OAAO,WAAW,QAAA,EAAU,OAAO,KAAK,EAAE,KAAA,EAAO,MAAA,EAAO,EAAG,GAAG,CAAA;AAElE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,YAAA,CAAa,EAAE,UAAU,MAAA,EAAQ,KAAA,EAAO,YAAA,EAAc,SAAA,EAAW,QAAQ,QAAA,EAAU,MAAA,EAAQ,MAAA,EAAQ,GAAA,CAAI,QAAQ,CAAA;AAEtI,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,MAAM,IAAA,GAAgB,MAAM,QAAA,CAAS,IAAA,EAAK;AAC1C,QAAA,OAAO,IAAA,CAAK,EAAE,KAAA,EAAO,YAAA,CAAa,UAAU,IAAI,CAAA,IAAK,GAAG,CAAA;AAAA,MAC1D;AAEA,MAAA,IAAI,CAAC,QAAA,CAAS,IAAA,EAAM,MAAM,IAAI,MAAM,mCAAmC,CAAA;AACvE,MAAA,OAAO,IAAI,QAAA,CAAS,WAAA,CAAY,UAAU,QAAA,CAAS,IAAA,EAAM,OAAO,CAAA,EAAG;AAAA,QACjE,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB,kCAAA;AAAA,UAChB,eAAA,EAAiB,wBAAA;AAAA,UACjB,UAAA,EAAY,YAAA;AAAA,UACZ,mBAAA,EAAqB;AAAA;AACvB,OACD,CAAA;AAAA,IACH,SAAS,GAAA,EAAK;AACZ,MAAA,OAAA,GAAU,GAAG,CAAA;AACb,MAAA,OAAO,IAAA,CAAK,EAAE,KAAA,EAAO,aAAA,IAAiB,GAAG,CAAA;AAAA,IAC3C;AAAA,EACF,CAAA;AACF;AAEA,SAAS,IAAA,CAAK,IAAA,EAAe,MAAA,EAAgB,OAAA,EAA4C;AACvF,EAAA,OAAO,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG;AAAA,IACxC,MAAA;AAAA,IACA,SAAS,EAAE,cAAA,EAAgB,mCAAmC,eAAA,EAAiB,UAAA,EAAY,GAAG,OAAA;AAAQ,GACvG,CAAA;AACH;AAGA,SAAS,aAAA,CAAc,IAAA,EAAe,WAAA,EAAqB,SAAA,EAA2C;AACpG,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAS,IAAA,CAAgC,QAAQ,CAAA,EAAG;AAClG,IAAA,OAAO,qDAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAO,IAAA,CAAiC,QAAA;AAC9C,EAAA,IAAI,GAAA,CAAI,MAAA,KAAW,CAAA,EAAG,OAAO,4BAAA;AAC7B,EAAA,IAAI,GAAA,CAAI,MAAA,GAAS,WAAA,EAAa,OAAO,0BAA0B,WAAW,CAAA,CAAA,CAAA;AAE1E,EAAA,MAAM,WAA0B,EAAC;AACjC,EAAA,KAAA,MAAW,KAAK,GAAA,EAAK;AACnB,IAAA,IAAI,CAAC,CAAA,IAAK,OAAO,CAAA,KAAM,UAAU,OAAO,gCAAA;AACxC,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,CAAA;AAC1B,IAAA,IAAI,IAAA,KAAS,MAAA,IAAU,IAAA,KAAS,WAAA,EAAa,OAAO,4CAAA;AACpD,IAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,CAAC,OAAA,CAAQ,IAAA,IAAQ,OAAO,4CAAA;AAC3D,IAAA,IAAI,OAAA,CAAQ,MAAA,GAAS,SAAA,EAAW,OAAO,yBAAyB,SAAS,CAAA,YAAA,CAAA;AACzE,IAAA,QAAA,CAAS,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,CAAA;AAAA,EACjC;AACA,EAAA,IAAI,QAAA,CAAS,CAAC,CAAA,EAAG,IAAA,KAAS,QAAQ,OAAO,yCAAA;AACzC,EAAA,IAAI,SAAS,QAAA,CAAS,MAAA,GAAS,CAAC,CAAA,EAAG,IAAA,KAAS,QAAQ,OAAO,wCAAA;AAC3D,EAAA,OAAO,QAAA;AACT;AAaA,eAAe,YAAA,CAAa,EAAE,QAAA,EAAU,MAAA,EAAQ,KAAA,EAAO,cAAc,SAAA,EAAW,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAO,EAAoC;AAC3I,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI,aAAa,WAAA,EAAa;AAC5B,IAAA,GAAA,GAAM,MAAM,MAAM,aAAA,EAAe;AAAA,MAC/B,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,WAAA,EAAa,MAAA;AAAA,QACb,mBAAA,EAAqB;AAAA,OACvB;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACnB,KAAA;AAAA,QACA,UAAA,EAAY,SAAA;AAAA,QACZ,QAAA;AAAA,QACA,GAAI,YAAA,GAAe,EAAE,MAAA,EAAQ,YAAA,KAAiB,EAAC;AAAA,QAC/C,GAAI,MAAA,GAAS,EAAE,MAAA,EAAQ,IAAA,KAAS;AAAC,OAClC,CAAA;AAAA,MACD;AAAA,KACD,CAAA;AAAA,EACH,CAAA,MAAO;AACL,IAAA,GAAA,GAAM,MAAM,MAAM,UAAA,EAAY;AAAA,MAC5B,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,aAAA,EAAe,UAAU,MAAM,CAAA;AAAA,OACjC;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACnB,KAAA;AAAA,QACA,qBAAA,EAAuB,SAAA;AAAA,QACvB,QAAA,EAAU,YAAA,GAAe,CAAC,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,YAAA,EAAa,EAAG,GAAG,QAAQ,CAAA,GAAI,QAAA;AAAA,QACpF,GAAI,MAAA,GAAS,EAAE,MAAA,EAAQ,IAAA,KAAS;AAAC,OAClC,CAAA;AAAA,MACD;AAAA,KACD,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,IAAA,MAAM,IAAI,cAAc,QAAA,EAAU,GAAA,CAAI,QAAQ,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAI,CAAC,CAAA;AAAA,EACnE;AACA,EAAA,OAAO,GAAA;AACT;AAEA,SAAS,YAAA,CAAa,UAAwB,IAAA,EAAuB;AACnE,EAAA,IAAI,aAAa,WAAA,EAAa;AAC5B,IAAA,MAAM,UAAW,IAAA,EAAgE,OAAA;AACjF,IAAA,IAAI,CAAC,MAAM,OAAA,CAAQ,OAAO,GAAG,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAClF,IAAA,OAAO,QACJ,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,IAAA,KAAS,UAAU,OAAO,CAAA,CAAE,SAAS,QAAQ,CAAA,CAC7D,IAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA,CACjB,KAAK,EAAE,CAAA;AAAA,EACZ;AACA,EAAA,MAAM,IAAA,GAAQ,IAAA,EAAmE,OAAA,GAAU,CAAC,GAAG,OAAA,EAAS,OAAA;AACxG,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,EAAU,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAChF,EAAA,OAAO,IAAA;AACT;AAGA,SAAS,YAAA,CAAa,QAAA,EAAwB,KAAA,EAAe,IAAA,EAAyE;AACpI,EAAA,IAAI,aAAa,QAAA,EAAU;AACzB,IAAA,IAAI,IAAA,KAAS,QAAA,EAAU,OAAO,EAAE,MAAM,IAAA,EAAK;AAC3C,IAAA,MAAMA,OAAAA,GAAS,UAAU,IAAI,CAAA;AAC7B,IAAA,IAAIA,OAAAA,EAAQ,KAAA,EAAO,OAAO,EAAE,OAAO,gBAAA,EAAiB;AACpD,IAAA,MAAM,OAAA,GAAUA,OAAAA,EAAQ,OAAA,GAAU,CAAC,GAAG,KAAA,EAAO,OAAA;AAC7C,IAAA,OAAO,OAAO,OAAA,KAAY,QAAA,IAAY,UAAU,EAAE,KAAA,EAAO,SAAQ,GAAI,IAAA;AAAA,EACvE;AAEA,EAAA,MAAM,MAAA,GAAS,UAAU,IAAI,CAAA;AAC7B,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,KAAA;AAC7B,EAAA,IAAI,IAAA,KAAS,cAAA,EAAgB,OAAO,EAAE,MAAM,IAAA,EAAK;AACjD,EAAA,IAAI,IAAA,KAAS,OAAA,EAAS,OAAO,EAAE,OAAO,gBAAA,EAAiB;AACvD,EAAA,IAAI,IAAA,KAAS,qBAAA,IAAyB,MAAA,EAAQ,KAAA,EAAO,IAAA,KAAS,gBAAgB,OAAO,MAAA,CAAO,KAAA,CAAM,IAAA,KAAS,QAAA,EAAU;AACnH,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,IAAA,EAAK;AAAA,EACpC;AACA,EAAA,OAAO,IAAA;AACT;AAGA,SAAS,WAAA,CAAY,QAAA,EAAwB,QAAA,EAAsC,OAAA,EAAgE;AACjJ,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,MAAM,MAAA,GAAS,SAAS,SAAA,EAAU;AAClC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,IAAI,QAAA,GAAW,KAAA;AAEf,EAAA,OAAO,IAAI,cAAA,CAA2B;AAAA,IACpC,MAAM,KAAK,UAAA,EAAY;AACrB,MAAA,MAAM,IAAA,GAAO,CAAC,OAAA,EAAiB,KAAA,KAC7B,UAAA,CAAW,OAAA,CAAQ,OAAA,CAAQ,MAAA,CAAO,CAAA,EAAG,KAAA,GAAQ,CAAA,OAAA,EAAU,KAAK;AAAA,CAAA,GAAO,EAAE,SAAS,OAAO;;AAAA,CAAM,CAAC,CAAA;AAC9F,MAAA,MAAM,SAAS,MAAM;AACnB,QAAA,IAAI,QAAA,EAAU;AACd,QAAA,QAAA,GAAW,IAAA;AACX,QAAA,IAAA,CAAK,QAAQ,CAAA;AACb,QAAA,UAAA,CAAW,KAAA,EAAM;AACjB,QAAA,MAAA,CAAO,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,QAAC,CAAC,CAAA;AAAA,MAChC,CAAA;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,QAAA,IAAI,IAAA,EAAM;AACR,UAAA,MAAA,IAAU,QAAQ,MAAA,EAAO;AACzB,UAAA,IAAI,MAAA,CAAO,IAAA,EAAK,EAAG,YAAA,CAAa,MAAM,CAAA;AACtC,UAAA,MAAA,EAAO;AACP,UAAA;AAAA,QACF;AACA,QAAA,MAAA,IAAU,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAChD,QAAA,MAAM,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,YAAY,CAAA;AACxC,QAAA,MAAA,GAAS,MAAA,CAAO,KAAI,IAAK,EAAA;AACzB,QAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,UAAA,YAAA,CAAa,KAAK,CAAA;AAClB,UAAA,IAAI,QAAA,EAAU;AAAA,QAChB;AAAA,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,QAAA,EAAU;AACd,QAAA,OAAA,GAAU,GAAG,CAAA;AACb,QAAA,IAAA,CAAK,KAAK,SAAA,CAAU,EAAE,OAAO,aAAA,EAAe,GAAG,OAAO,CAAA;AACtD,QAAA,MAAA,EAAO;AAAA,MACT;AAEA,MAAA,SAAS,aAAa,KAAA,EAAe;AACnC,QAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,QAAA,MAAM,YAAsB,EAAC;AAC7B,QAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA,EAAG;AACvC,UAAA,IAAI,IAAA,CAAK,WAAW,QAAQ,CAAA,UAAW,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,EAAK;AAAA,eAAA,IACjD,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG,SAAA,CAAU,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA;AAAA,QACnF;AACA,QAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC5B,QAAA,MAAM,SAAS,YAAA,CAAa,QAAA,EAAU,OAAO,SAAA,CAAU,IAAA,CAAK,IAAI,CAAC,CAAA;AACjE,QAAA,IAAI,CAAC,MAAA,EAAQ;AACb,QAAA,IAAI,OAAO,KAAA,EAAO;AAChB,UAAA,OAAA,GAAU,IAAI,KAAA,CAAM,CAAA,EAAG,QAAQ,kBAAkB,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,GAAI,CAAC,EAAE,CAAC,CAAA;AACvF,UAAA,IAAA,CAAK,KAAK,SAAA,CAAU,EAAE,OAAO,aAAA,EAAe,GAAG,OAAO,CAAA;AACtD,UAAA,MAAA,EAAO;AAAA,QACT,CAAA,MAAA,IAAW,OAAO,KAAA,EAAO;AACvB,UAAA,IAAA,CAAK,KAAK,SAAA,CAAU,EAAE,OAAO,MAAA,CAAO,KAAA,EAAO,CAAC,CAAA;AAAA,QAC9C,CAAA,MAAA,IAAW,OAAO,IAAA,EAAM;AACtB,UAAA,MAAA,EAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,OAAO,MAAA,EAAQ;AACb,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,OAAO,MAAA,CAAO,OAAO,MAAM,CAAA;AAAA,IAC7B;AAAA,GACD,CAAA;AACH;AAEA,SAAS,UAAU,IAAA,EAAuB;AACxC,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACxB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF","file":"server.js","sourcesContent":["import type { ChatMessage } from '../types';\n\nexport type ChatProvider = 'anthropic' | 'openai';\n\nexport interface CreateChatHandlerOptions {\n provider: ChatProvider;\n /** Provider API key. Read it from an env var on the server, never ship it to the browser. */\n apiKey: string;\n /** Defaults: `claude-sonnet-4-5` (anthropic), `gpt-4o-mini` (openai). */\n model?: string;\n systemPrompt?: string;\n /** Max tokens for the reply. Default: 1024. */\n maxTokens?: number;\n /** Respond with an SSE stream (`text/event-stream`) instead of JSON. Default: false. */\n stream?: boolean;\n /** Max messages accepted per request (oldest are rejected beyond this). Default: 50. */\n maxMessages?: number;\n /** Max characters per message. Default: 8000. */\n maxMessageLength?: number;\n /** Server-side hook for logging upstream / internal errors. The client only ever sees a generic message. */\n onError?: (error: unknown) => void;\n}\n\nexport type ChatHandler = (req: Request) => Promise<Response>;\n\nexport const DEFAULT_MODELS: Record<ChatProvider, string> = {\n anthropic: 'claude-sonnet-4-5',\n openai: 'gpt-4o-mini',\n};\n\nconst ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';\nconst ANTHROPIC_VERSION = '2023-06-01';\nconst OPENAI_URL = 'https://api.openai.com/v1/chat/completions';\n\nconst GENERIC_ERROR = 'Something went wrong. Please try again.';\n\nclass UpstreamError extends Error {\n constructor(\n readonly provider: ChatProvider,\n readonly status: number,\n readonly body: string,\n ) {\n super(`${provider} API responded with status ${status}`);\n this.name = 'UpstreamError';\n }\n}\n\nexport function createChatHandler(options: CreateChatHandlerOptions): ChatHandler {\n const {\n provider,\n apiKey,\n model = DEFAULT_MODELS[provider],\n systemPrompt,\n maxTokens = 1024,\n stream = false,\n maxMessages = 50,\n maxMessageLength = 8000,\n onError,\n } = options;\n\n if (provider !== 'anthropic' && provider !== 'openai') {\n throw new Error(`Unsupported provider: ${String(provider)}`);\n }\n\n return async function chatHandler(req: Request): Promise<Response> {\n if (req.method !== 'POST') {\n return json({ error: 'Method not allowed' }, 405, { Allow: 'POST' });\n }\n\n if (!apiKey) {\n return json({ error: 'Chat backend is not configured' }, 401);\n }\n\n let body: unknown;\n try {\n body = await req.json();\n } catch {\n return json({ error: 'Invalid JSON body' }, 400);\n }\n\n const parsed = parseMessages(body, maxMessages, maxMessageLength);\n if (typeof parsed === 'string') return json({ error: parsed }, 400);\n\n try {\n const upstream = await callProvider({ provider, apiKey, model, systemPrompt, maxTokens, stream, messages: parsed, signal: req.signal });\n\n if (!stream) {\n const data: unknown = await upstream.json();\n return json({ reply: extractReply(provider, data) }, 200);\n }\n\n if (!upstream.body) throw new Error('Upstream returned an empty stream');\n return new Response(toClientSSE(provider, upstream.body, onError), {\n status: 200,\n headers: {\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n });\n } catch (err) {\n onError?.(err);\n return json({ error: GENERIC_ERROR }, 500);\n }\n };\n}\n\nfunction json(data: unknown, status: number, headers?: Record<string, string>): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', ...headers },\n });\n}\n\n/** Returns the validated messages, or an error string. Only `user`/`assistant` roles are accepted: the system prompt is server-controlled. */\nfunction parseMessages(body: unknown, maxMessages: number, maxLength: number): ChatMessage[] | string {\n if (!body || typeof body !== 'object' || !Array.isArray((body as { messages?: unknown }).messages)) {\n return 'Body must be { messages: Array<{ role, content }> }';\n }\n const raw = (body as { messages: unknown[] }).messages;\n if (raw.length === 0) return 'messages must not be empty';\n if (raw.length > maxMessages) return `Too many messages (max ${maxMessages})`;\n\n const messages: ChatMessage[] = [];\n for (const m of raw) {\n if (!m || typeof m !== 'object') return 'Each message must be an object';\n const { role, content } = m as { role?: unknown; content?: unknown };\n if (role !== 'user' && role !== 'assistant') return 'Message role must be \"user\" or \"assistant\"';\n if (typeof content !== 'string' || !content.trim()) return 'Message content must be a non-empty string';\n if (content.length > maxLength) return `Message too long (max ${maxLength} characters)`;\n messages.push({ role, content });\n }\n if (messages[0]?.role !== 'user') return 'The first message must be from the user';\n if (messages[messages.length - 1]?.role !== 'user') return 'The last message must be from the user';\n return messages;\n}\n\ninterface ProviderCall {\n provider: ChatProvider;\n apiKey: string;\n model: string;\n systemPrompt?: string;\n maxTokens: number;\n stream: boolean;\n messages: ChatMessage[];\n signal?: AbortSignal;\n}\n\nasync function callProvider({ provider, apiKey, model, systemPrompt, maxTokens, stream, messages, signal }: ProviderCall): Promise<Response> {\n let res: Response;\n if (provider === 'anthropic') {\n res = await fetch(ANTHROPIC_URL, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': ANTHROPIC_VERSION,\n },\n body: JSON.stringify({\n model,\n max_tokens: maxTokens,\n messages,\n ...(systemPrompt ? { system: systemPrompt } : {}),\n ...(stream ? { stream: true } : {}),\n }),\n signal,\n });\n } else {\n res = await fetch(OPENAI_URL, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n max_completion_tokens: maxTokens,\n messages: systemPrompt ? [{ role: 'system', content: systemPrompt }, ...messages] : messages,\n ...(stream ? { stream: true } : {}),\n }),\n signal,\n });\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new UpstreamError(provider, res.status, text.slice(0, 2000));\n }\n return res;\n}\n\nfunction extractReply(provider: ChatProvider, data: unknown): string {\n if (provider === 'anthropic') {\n const content = (data as { content?: Array<{ type?: string; text?: string }> })?.content;\n if (!Array.isArray(content)) throw new Error('Unexpected Anthropic response shape');\n return content\n .filter((b) => b.type === 'text' && typeof b.text === 'string')\n .map((b) => b.text)\n .join('');\n }\n const text = (data as { choices?: Array<{ message?: { content?: unknown } }> })?.choices?.[0]?.message?.content;\n if (typeof text !== 'string') throw new Error('Unexpected OpenAI response shape');\n return text;\n}\n\n/** Extracts a text delta from one upstream SSE event. Returns `null` for non-text events. */\nfunction extractDelta(provider: ChatProvider, event: string, data: string): { delta?: string; done?: boolean; error?: string } | null {\n if (provider === 'openai') {\n if (data === '[DONE]') return { done: true };\n const parsed = safeParse(data) as { choices?: Array<{ delta?: { content?: unknown } }>; error?: unknown } | undefined;\n if (parsed?.error) return { error: 'upstream error' };\n const content = parsed?.choices?.[0]?.delta?.content;\n return typeof content === 'string' && content ? { delta: content } : null;\n }\n\n const parsed = safeParse(data) as { type?: string; delta?: { type?: string; text?: unknown } } | undefined;\n const type = parsed?.type ?? event;\n if (type === 'message_stop') return { done: true };\n if (type === 'error') return { error: 'upstream error' };\n if (type === 'content_block_delta' && parsed?.delta?.type === 'text_delta' && typeof parsed.delta.text === 'string') {\n return { delta: parsed.delta.text };\n }\n return null;\n}\n\n/** Re-encodes the provider stream as the widget's SSE protocol: `data: {\"delta\"}` … `data: [DONE]`. */\nfunction toClientSSE(provider: ChatProvider, upstream: ReadableStream<Uint8Array>, onError?: (error: unknown) => void): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n const decoder = new TextDecoder();\n const reader = upstream.getReader();\n let buffer = '';\n let finished = false;\n\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n const send = (payload: string, event?: string) =>\n controller.enqueue(encoder.encode(`${event ? `event: ${event}\\n` : ''}data: ${payload}\\n\\n`));\n const finish = () => {\n if (finished) return;\n finished = true;\n send('[DONE]');\n controller.close();\n reader.cancel().catch(() => {});\n };\n\n try {\n const { done, value } = await reader.read();\n if (done) {\n buffer += decoder.decode();\n if (buffer.trim()) processBlock(buffer);\n finish();\n return;\n }\n buffer += decoder.decode(value, { stream: true });\n const blocks = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = blocks.pop() ?? '';\n for (const block of blocks) {\n processBlock(block);\n if (finished) return;\n }\n } catch (err) {\n if (finished) return;\n onError?.(err);\n send(JSON.stringify({ error: GENERIC_ERROR }), 'error');\n finish();\n }\n\n function processBlock(block: string) {\n let event = '';\n const dataLines: string[] = [];\n for (const line of block.split(/\\r?\\n/)) {\n if (line.startsWith('event:')) event = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return;\n const result = extractDelta(provider, event, dataLines.join('\\n'));\n if (!result) return;\n if (result.error) {\n onError?.(new Error(`${provider} stream error: ${dataLines.join('\\n').slice(0, 2000)}`));\n send(JSON.stringify({ error: GENERIC_ERROR }), 'error');\n finish();\n } else if (result.delta) {\n send(JSON.stringify({ delta: result.delta }));\n } else if (result.done) {\n finish();\n }\n }\n },\n cancel(reason) {\n finished = true;\n return reader.cancel(reason);\n },\n });\n}\n\nfunction safeParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n"]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Optional enhancements for @trossino/chat-widget.
|
|
3
|
+
* The widget is fully styled inline and works without this file.
|
|
4
|
+
* Import it to get things inline styles can't express (focus rings, hover, reduced motion):
|
|
5
|
+
* import '@trossino/chat-widget/styles';
|
|
6
|
+
*/
|
|
7
|
+
.tr-chat-widget button:focus-visible,
|
|
8
|
+
.tr-chat-widget textarea:focus-visible {
|
|
9
|
+
outline: 2px solid #1d4ed8;
|
|
10
|
+
outline-offset: 2px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.tr-chat-toggle:hover,
|
|
14
|
+
.tr-chat-send:not(:disabled):hover {
|
|
15
|
+
filter: brightness(1.08);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.tr-chat-panel {
|
|
19
|
+
animation: tr-chat-in 140ms ease-out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes tr-chat-in {
|
|
23
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
24
|
+
to { opacity: 1; transform: translateY(0); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-reduced-motion: reduce) {
|
|
28
|
+
.tr-chat-panel { animation: none; }
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trossino/chat-widget",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Batteries-included React chat widget with optional server-side handler for AI backends (Anthropic, OpenAI).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Tommaso Rossino",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"./server": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/server.d.ts",
|
|
25
|
+
"default": "./dist/server.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/server.d.cts",
|
|
29
|
+
"default": "./dist/server.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./styles": "./dist/styles.css",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"typesVersions": {
|
|
36
|
+
"*": {
|
|
37
|
+
"server": [
|
|
38
|
+
"./dist/server.d.ts"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"sideEffects": [
|
|
46
|
+
"*.css"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": "^18 || ^19",
|
|
56
|
+
"react-dom": "^18 || ^19"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^24.0.0",
|
|
60
|
+
"@types/react": "^19.0.0",
|
|
61
|
+
"@types/react-dom": "^19.0.0",
|
|
62
|
+
"react": "^19.0.0",
|
|
63
|
+
"react-dom": "^19.0.0",
|
|
64
|
+
"tsup": "^8.3.5",
|
|
65
|
+
"typescript": "~5.7.3"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "git+https://github.com/tommaso-rossino/ai-chat-widget.git"
|
|
76
|
+
},
|
|
77
|
+
"homepage": "https://github.com/tommaso-rossino/ai-chat-widget#readme",
|
|
78
|
+
"bugs": {
|
|
79
|
+
"url": "https://github.com/tommaso-rossino/ai-chat-widget/issues"
|
|
80
|
+
},
|
|
81
|
+
"keywords": [
|
|
82
|
+
"react",
|
|
83
|
+
"chat",
|
|
84
|
+
"widget",
|
|
85
|
+
"ai",
|
|
86
|
+
"anthropic",
|
|
87
|
+
"claude",
|
|
88
|
+
"openai",
|
|
89
|
+
"sse",
|
|
90
|
+
"nextjs"
|
|
91
|
+
]
|
|
92
|
+
}
|