├── .cursor ├── mcp.json └── rules │ ├── cursor_rules.mdc │ ├── dev_workflow.mdc │ ├── self_improve.mdc │ └── taskmaster.mdc ├── .env ├── .env.example ├── .env.production ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .roo ├── rules-architect │ └── architect-rules ├── rules-ask │ └── ask-rules ├── rules-boomerang │ └── boomerang-rules ├── rules-code │ └── code-rules ├── rules-debug │ └── debug-rules ├── rules-test │ └── test-rules └── rules │ ├── dev_workflow.md │ ├── roo_rules.md │ ├── self_improve.md │ └── taskmaster.md ├── .roomodes ├── .taskmasterconfig ├── .vscode ├── i18n-ally-reviews.yml ├── launch.json └── settings.json ├── .windsurfrules ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _locales ├── ar │ ├── description.txt │ └── messages.json ├── bg │ ├── description.txt │ └── messages.json ├── bn │ ├── description.txt │ └── messages.json ├── cs │ ├── description.txt │ └── messages.json ├── da │ ├── description.txt │ └── messages.json ├── de │ ├── description.txt │ └── messages.json ├── el │ ├── description.txt │ └── messages.json ├── en │ ├── description.txt │ └── messages.json ├── es │ ├── description.txt │ └── messages.json ├── fi │ ├── description.txt │ └── messages.json ├── fr │ ├── description.txt │ └── messages.json ├── hi │ ├── description.txt │ └── messages.json ├── hr │ ├── description.txt │ └── messages.json ├── hu │ ├── description.txt │ └── messages.json ├── id │ ├── description.txt │ └── messages.json ├── it │ ├── description.txt │ └── messages.json ├── ja │ ├── description.txt │ └── messages.json ├── ko │ ├── description.txt │ └── messages.json ├── ms │ ├── description.txt │ └── messages.json ├── nl │ ├── description.txt │ └── messages.json ├── pl │ ├── description.txt │ └── messages.json ├── pt_BR │ ├── description.txt │ └── messages.json ├── ro │ ├── description.txt │ └── messages.json ├── ru │ ├── description.txt │ └── messages.json ├── sk │ ├── description.txt │ └── messages.json ├── sv │ ├── description.txt │ └── messages.json ├── ta │ ├── description.txt │ └── messages.json ├── tl │ ├── description.txt │ └── messages.json ├── tr │ ├── description.txt │ └── messages.json ├── uk │ ├── description.txt │ └── messages.json ├── vi │ ├── description.txt │ └── messages.json └── zh_CN │ ├── description.txt │ └── messages.json ├── copy-onnx-files.js ├── doc ├── Modules.md ├── auth │ ├── auth-refresh.md │ └── auth.md ├── personalisation_feature_plan.md └── plans │ └── jwt-plan.md ├── i18n-clear-keys.py ├── i18n-delete-keys.py ├── i18n-placeholders.py ├── i18n-product-names.py ├── i18n-translate-release-text.py ├── i18n-validate.cjs ├── manifest.json ├── ngrok.yml ├── package-extension.sh ├── package-lock.json ├── package.json ├── public ├── 58ee4d2d6f295eb74cf1.svg ├── 6d076abcfd16e9f4ad0b.png ├── audio │ ├── attention-1.mp3 │ ├── attention-2.mp3 │ ├── beep-off.mp3 │ ├── beep-on.mp3 │ ├── call-failed.mp3 │ ├── send-round-long.mp3 │ ├── send-round-short.mp3 │ ├── startup-synth.mp3 │ ├── switch-off.mp3 │ ├── switch-on.mp3 │ ├── test-tone.mp3 │ └── turn-off.mp3 ├── d54c339478aa04913da7.svg ├── icons │ ├── logos │ │ ├── elevenlabs.svg │ │ ├── inflection.ai.png │ │ ├── openai.svg │ │ └── saypi.png │ ├── microphone-muted.svg │ ├── microphone-switch.svg │ ├── microphone.svg │ └── sixty-seconds.svg ├── logos │ └── marquee.png ├── ort-wasm-simd-threaded.jsep.mjs ├── ort-wasm-simd-threaded.mjs ├── silero_vad.onnx ├── silero_vad_legacy.onnx ├── silero_vad_v5.onnx └── vad.worklet.bundle.js ├── scripts ├── PRD-transcription-status-indicator.txt ├── PRD_Media_CSP_Resolution.md ├── PRD_VAD_CSP_Resolution.md ├── example_prd.txt ├── prd.txt └── update-manifest.js ├── server.js ├── src ├── AnalyticsModule.ts ├── AnimationModule.js ├── ApiClient.ts ├── ButtonModule.js ├── CacheBuster.ts ├── ConfigModule.js ├── FullscreenModule.ts ├── ImmersionService.js ├── ImmersionServiceLite.ts ├── JwtManager.ts ├── LoggingModule.js ├── NotificationsModule.ts ├── RequestInterceptor.js ├── ResourceModule.ts ├── SlowResponseHandler.ts ├── StateMachineService.js ├── SubmitErrorHandler.ts ├── TelemetryModule.ts ├── TextModule.ts ├── TimerModule.ts ├── TranscriptMergeService.ts ├── TranscriptionModule.ts ├── UserAgentModule.ts ├── WakeLockModule.ts ├── __mocks__ │ ├── ConfigModule.ts │ ├── message-hover-menu-claude.html │ └── message-popup.html ├── audio │ ├── AudioCapabilities.ts │ ├── AudioControlsModule.ts │ ├── AudioEncoder.ts │ ├── AudioEvents.ts │ ├── AudioModule.js │ ├── OffscreenAudioBridge.js │ ├── SlowResponseHandlerAdapter.js │ ├── WavEncoder.ts │ └── capabilities-test.json ├── billing │ └── BillingModule.ts ├── buttons │ ├── CallButton.ts │ └── GlowColorUpdater.js ├── chatbots │ ├── AbstractChatbots.ts │ ├── Chatbot.ts │ ├── ChatbotIdentifier.ts │ ├── ChatbotService.ts │ ├── Claude.ts │ ├── ClaudeVoiceMenu.ts │ ├── Pi.ts │ ├── PiVoiceMenu.ts │ └── bootstrap.ts ├── dom │ ├── BaseObserver.ts │ ├── ChatHistory.ts │ ├── DOMModule.ts │ ├── MessageElements.ts │ ├── MessageEvents.ts │ └── Observation.ts ├── error-management │ └── TranscriptionErrorManager.ts ├── events │ ├── EventBus.js │ └── EventModule.js ├── i18n.ts ├── icons │ ├── IconModule.ts │ ├── brain.svg │ ├── bubble-128px.png │ ├── bubble-16px.png │ ├── bubble-300px.png │ ├── bubble-32px.png │ ├── bubble-48px.png │ ├── bubble-bw.svg │ ├── call-starting.svg │ ├── call.svg │ ├── claude-chevron.svg │ ├── copied.svg │ ├── copy.svg │ ├── exit.svg │ ├── flags │ │ ├── ad.svg │ │ ├── ae.svg │ │ ├── af.svg │ │ ├── ag.svg │ │ ├── ai.svg │ │ ├── al.svg │ │ ├── am.svg │ │ ├── ao.svg │ │ ├── aq.svg │ │ ├── ar.svg │ │ ├── arab.svg │ │ ├── as.svg │ │ ├── at.svg │ │ ├── au.svg │ │ ├── aw.svg │ │ ├── ax.svg │ │ ├── az.svg │ │ ├── ba.svg │ │ ├── bb.svg │ │ ├── bd.svg │ │ ├── be.svg │ │ ├── bf.svg │ │ ├── bg.svg │ │ ├── bh.svg │ │ ├── bi.svg │ │ ├── bj.svg │ │ ├── bl.svg │ │ ├── bm.svg │ │ ├── bn.svg │ │ ├── bo.svg │ │ ├── bq.svg │ │ ├── br.svg │ │ ├── bs.svg │ │ ├── bt.svg │ │ ├── bv.svg │ │ ├── bw.svg │ │ ├── by.svg │ │ ├── bz.svg │ │ ├── ca.svg │ │ ├── cc.svg │ │ ├── cd.svg │ │ ├── cefta.svg │ │ ├── cf.svg │ │ ├── cg.svg │ │ ├── ch.svg │ │ ├── ci.svg │ │ ├── ck.svg │ │ ├── cl.svg │ │ ├── cm.svg │ │ ├── cn.svg │ │ ├── co.svg │ │ ├── cp.svg │ │ ├── cr.svg │ │ ├── cu.svg │ │ ├── cv.svg │ │ ├── cw.svg │ │ ├── cx.svg │ │ ├── cy.svg │ │ ├── cz.svg │ │ ├── de.svg │ │ ├── dg.svg │ │ ├── dj.svg │ │ ├── dk.svg │ │ ├── dm.svg │ │ ├── do.svg │ │ ├── dz.svg │ │ ├── eac.svg │ │ ├── ec.svg │ │ ├── ee.svg │ │ ├── eg.svg │ │ ├── eh.svg │ │ ├── er.svg │ │ ├── es-ct.svg │ │ ├── es-ga.svg │ │ ├── es-pv.svg │ │ ├── es.svg │ │ ├── et.svg │ │ ├── eu.svg │ │ ├── fi.svg │ │ ├── fj.svg │ │ ├── fk.svg │ │ ├── fm.svg │ │ ├── fo.svg │ │ ├── fr.svg │ │ ├── ga.svg │ │ ├── gb-eng.svg │ │ ├── gb-nir.svg │ │ ├── gb-sct.svg │ │ ├── gb-wls.svg │ │ ├── gb.svg │ │ ├── gd.svg │ │ ├── ge.svg │ │ ├── gf.svg │ │ ├── gg.svg │ │ ├── gh.svg │ │ ├── gi.svg │ │ ├── gl.svg │ │ ├── global.svg │ │ ├── gm.svg │ │ ├── gn.svg │ │ ├── gp.svg │ │ ├── gq.svg │ │ ├── gr.svg │ │ ├── gs.svg │ │ ├── gt.svg │ │ ├── gu.svg │ │ ├── gw.svg │ │ ├── gy.svg │ │ ├── hk.svg │ │ ├── hm.svg │ │ ├── hn.svg │ │ ├── hr.svg │ │ ├── ht.svg │ │ ├── hu.svg │ │ ├── ic.svg │ │ ├── id.svg │ │ ├── ie.svg │ │ ├── il.svg │ │ ├── im.svg │ │ ├── in.svg │ │ ├── io.svg │ │ ├── iq.svg │ │ ├── ir.svg │ │ ├── is.svg │ │ ├── it.svg │ │ ├── je.svg │ │ ├── jm.svg │ │ ├── jo.svg │ │ ├── jp.svg │ │ ├── ke.svg │ │ ├── kg.svg │ │ ├── kh.svg │ │ ├── ki.svg │ │ ├── km.svg │ │ ├── kn.svg │ │ ├── kp.svg │ │ ├── kr.svg │ │ ├── kw.svg │ │ ├── ky.svg │ │ ├── kz.svg │ │ ├── la.svg │ │ ├── lb.svg │ │ ├── lc.svg │ │ ├── li.svg │ │ ├── lk.svg │ │ ├── lr.svg │ │ ├── ls.svg │ │ ├── lt.svg │ │ ├── lu.svg │ │ ├── lv.svg │ │ ├── ly.svg │ │ ├── ma.svg │ │ ├── mc.svg │ │ ├── md.svg │ │ ├── me.svg │ │ ├── mf.svg │ │ ├── mg.svg │ │ ├── mh.svg │ │ ├── mk.svg │ │ ├── ml.svg │ │ ├── mm.svg │ │ ├── mn.svg │ │ ├── mo.svg │ │ ├── mp.svg │ │ ├── mq.svg │ │ ├── mr.svg │ │ ├── ms.svg │ │ ├── mt.svg │ │ ├── mu.svg │ │ ├── mv.svg │ │ ├── mw.svg │ │ ├── mx.svg │ │ ├── my.svg │ │ ├── mz.svg │ │ ├── na.svg │ │ ├── nc.svg │ │ ├── ne.svg │ │ ├── nf.svg │ │ ├── ng.svg │ │ ├── ni.svg │ │ ├── nl.svg │ │ ├── no.svg │ │ ├── np.svg │ │ ├── nr.svg │ │ ├── nu.svg │ │ ├── nz.svg │ │ ├── om.svg │ │ ├── pa.svg │ │ ├── pc.svg │ │ ├── pe.svg │ │ ├── pf.svg │ │ ├── pg.svg │ │ ├── ph.svg │ │ ├── pk.svg │ │ ├── pl.svg │ │ ├── pm.svg │ │ ├── pn.svg │ │ ├── pr.svg │ │ ├── ps.svg │ │ ├── pt.svg │ │ ├── pw.svg │ │ ├── py.svg │ │ ├── qa.svg │ │ ├── re.svg │ │ ├── ro.svg │ │ ├── rs.svg │ │ ├── ru.svg │ │ ├── rw.svg │ │ ├── sa.svg │ │ ├── sb.svg │ │ ├── sc.svg │ │ ├── sd.svg │ │ ├── se.svg │ │ ├── sg.svg │ │ ├── sh-ac.svg │ │ ├── sh-hl.svg │ │ ├── sh-ta.svg │ │ ├── sh.svg │ │ ├── si.svg │ │ ├── sj.svg │ │ ├── sk.svg │ │ ├── sl.svg │ │ ├── sm.svg │ │ ├── sn.svg │ │ ├── so.svg │ │ ├── sr.svg │ │ ├── ss.svg │ │ ├── st.svg │ │ ├── sv.svg │ │ ├── sx.svg │ │ ├── sy.svg │ │ ├── system.svg │ │ ├── sz.svg │ │ ├── tc.svg │ │ ├── td.svg │ │ ├── tf.svg │ │ ├── tg.svg │ │ ├── th.svg │ │ ├── tj.svg │ │ ├── tk.svg │ │ ├── tl.svg │ │ ├── tm.svg │ │ ├── tn.svg │ │ ├── to.svg │ │ ├── tr.svg │ │ ├── tt.svg │ │ ├── tv.svg │ │ ├── tw.svg │ │ ├── tz.svg │ │ ├── ua.svg │ │ ├── ug.svg │ │ ├── um.svg │ │ ├── un.svg │ │ ├── us.svg │ │ ├── uy.svg │ │ ├── uz.svg │ │ ├── va.svg │ │ ├── vc.svg │ │ ├── ve.svg │ │ ├── vg.svg │ │ ├── vi.svg │ │ ├── vn.svg │ │ ├── vu.svg │ │ ├── wf.svg │ │ ├── ws.svg │ │ ├── xk.svg │ │ ├── xx.svg │ │ ├── ye.svg │ │ ├── yt.svg │ │ ├── za.svg │ │ ├── zm.svg │ │ └── zw.svg │ ├── hangup-minced.svg │ ├── hangup.svg │ ├── immersive.svg │ ├── interrupt.svg │ ├── lock.svg │ ├── maximize.svg │ ├── mode-day.svg │ ├── mode-night.svg │ ├── rectangles-moonlight.svg │ ├── rectangles.svg │ ├── regenerate.svg │ ├── settings.svg │ ├── steer.svg │ ├── stopwatch.svg │ ├── unlock.svg │ ├── volume-mid.svg │ ├── volume-muted.svg │ ├── wave.svg │ └── waveform.svg ├── metadata.txt ├── offscreen │ ├── audio_handler.ts │ ├── media_coordinator.ts │ ├── media_offscreen.html │ ├── media_offscreen.ts │ ├── offscreen_manager.ts │ └── vad_handler.ts ├── permissions │ ├── himfloyd-mic.png │ ├── permissions-prompt.css │ ├── permissions-prompt.html │ └── permissions-prompt.ts ├── popup │ ├── alert.svg │ ├── auth-shared.js │ ├── auth.js │ ├── beta.css │ ├── beta.svg │ ├── consent.css │ ├── data-sharing.png │ ├── language-picker.css │ ├── language-picker.js │ ├── popup-config.js │ ├── popup.html │ ├── popup.js │ ├── popupopener.ts │ ├── preferences.css │ ├── simple-user-agent.js │ ├── sketch.svg │ ├── status-subscription.js │ ├── status.css │ ├── status.js │ ├── toggle.css │ ├── trash.svg │ └── usage.css ├── prefs │ ├── PreferenceModule.ts │ └── __tests__ │ │ └── PreferenceModule.migration.test.ts ├── saypi.index.js ├── state-machines │ ├── AudioInputMachine.ts │ ├── AudioOutputMachine.ts │ ├── AudioRetryMachine.ts │ ├── FocusMachine.ts │ ├── SayPiMachine.ts │ ├── ScreenLockMachine.ts │ ├── SessionAnalyticsMachine.ts │ ├── ThemeToggleMachine.ts │ └── VoiceConverter.ts ├── styles │ ├── claude.scss │ ├── common.scss │ ├── dark-mode.scss │ ├── desktop.scss │ ├── focus-mode.scss │ ├── lock.scss │ ├── messages.scss │ ├── mobile.scss │ ├── neon.scss │ ├── notifications.scss │ ├── pi.scss │ ├── progress-ring.scss │ ├── rectangles.css │ └── voices.scss ├── svc │ └── background.ts ├── svg.d.ts ├── telemetry │ └── ui │ │ └── TelemetryVisualizer.ts ├── themes │ └── ThemeManagerModule.ts ├── tts │ ├── AudioStreamManager.ts │ ├── ChatHistoryManager.ts │ ├── FailedSpeechUtterance.ts │ ├── InputBuffer.ts │ ├── InputStream.ts │ ├── MessageHistoryModule.ts │ ├── README.md │ ├── SpeechFailureReason.ts │ ├── SpeechHistoryModule.ts │ ├── SpeechModel.ts │ ├── SpeechSourceParsers.ts │ ├── SpeechSynthesisModule.ts │ ├── TTSControlsModule.ts │ ├── TextToSpeechService.ts │ ├── VoiceMenu.ts │ ├── VoiceMenuUIManager.ts │ └── __mocks__ │ │ ├── SpeechSynthesisModule.ts │ │ └── voice-settings.html ├── ui │ └── VADStatusIndicator.ts └── vad │ ├── OffscreenVADClient.ts │ └── custom-model-fetcher.js ├── test ├── JwtManager.spec.ts ├── SessionAnalytics.spec.disabled.ts ├── UserAgentModule.spec.ts ├── audio │ └── AudioModule-OffscreenIntegration.spec.ts ├── cache.busting.spec.ts ├── chatbots │ └── ClaudeTextStream.spec.ts ├── data │ └── Voices.ts ├── dom │ ├── ChatHistoryObserver.spec.ts │ └── DOMModule.spec.ts ├── jest.setup.js ├── merge.test.ts ├── offscreen │ ├── audio_handler.spec.ts │ └── message-routing.spec.ts ├── prefs │ └── PreferenceModule.mock.ts ├── text.test.js ├── tts │ ├── AudioStreamManager.spec.ts │ ├── InputBuffer.spec.ts │ ├── InputStream.spec.ts │ ├── SpeechHistory.spec.ts │ ├── SpeechSourceParsers.spec.ts │ ├── SpeechSynthesisModule.spec.ts │ └── TextToSpeechService.spec.ts ├── utils │ └── dom.ts └── vitest.setup.js ├── tsconfig.json ├── vitest.config.js └── webpack.config.js /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "task-master-ai": { 4 | "command": "npx", 5 | "args": [ 6 | "-y", 7 | "--package=task-master-ai", 8 | "task-master-ai" 9 | ], 10 | "env": { 11 | "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE", 12 | "PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE", 13 | "OPENAI_API_KEY": "OPENAI_API_KEY_HERE", 14 | "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE", 15 | "XAI_API_KEY": "XAI_API_KEY_HERE", 16 | "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE", 17 | "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE", 18 | "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE", 19 | "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE" 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.cursor/rules/cursor_rules.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness. 3 | globs: .cursor/rules/*.mdc 4 | alwaysApply: true 5 | --- 6 | 7 | - **Required Rule Structure:** 8 | ```markdown 9 | --- 10 | description: Clear, one-line description of what the rule enforces 11 | globs: path/to/files/*.ext, other/path/**/* 12 | alwaysApply: boolean 13 | --- 14 | 15 | - **Main Points in Bold** 16 | - Sub-points with details 17 | - Examples and explanations 18 | ``` 19 | 20 | - **File References:** 21 | - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files 22 | - Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references 23 | - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references 24 | 25 | - **Code Examples:** 26 | - Use language-specific code blocks 27 | ```typescript 28 | // ✅ DO: Show good examples 29 | const goodExample = true; 30 | 31 | // ❌ DON'T: Show anti-patterns 32 | const badExample = false; 33 | ``` 34 | 35 | - **Rule Content Guidelines:** 36 | - Start with high-level overview 37 | - Include specific, actionable requirements 38 | - Show examples of correct implementation 39 | - Reference existing code when possible 40 | - Keep rules DRY by referencing other rules 41 | 42 | - **Rule Maintenance:** 43 | - Update rules when new patterns emerge 44 | - Add examples from actual codebase 45 | - Remove outdated patterns 46 | - Cross-reference related rules 47 | 48 | - **Best Practices:** 49 | - Use bullet points for clarity 50 | - Keep descriptions concise 51 | - Include both DO and DON'T examples 52 | - Reference actual code over theoretical examples 53 | - Use consistent formatting across rules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Server URLs 2 | APP_SERVER_URL=https://app.saypi.ai 3 | API_SERVER_URL=https://api.saypi.ai 4 | AUTH_SERVER_URL=https://www.saypi.ai 5 | #API_SERVER_URL=https://localhost:5001 6 | #AUTH_SERVER_URL=http://localhost:3000 7 | 8 | 9 | # JWT Configuration 10 | # AUTH_SESSION_LIFETIME=360 11 | 12 | # Google Analytics Configuration 13 | GA_MEASUREMENT_ID=G-N70M5T0ZCB 14 | GA_API_SECRET=4HsiqPmGS7Gs3TYWOa2ddg 15 | GA_ENDPOINT=https://www.google-analytics.com/debug/mp/collect 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Example environment variables 2 | # Copy this file to .env for development or .env.production for production builds 3 | 4 | # API server endpoint 5 | API_SERVER_URL=https://api.saypi.ai 6 | 7 | # Authentication server endpoint 8 | AUTH_SERVER_URL=https://www.saypi.ai 9 | 10 | # Google Analytics 11 | GA_MEASUREMENT_ID=G-XXXXXXXXXX 12 | GA_API_SECRET=XXXXXXXXXX 13 | GA_ENDPOINT=https://www.google-analytics.com/mp/collect 14 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | APP_SERVER_URL=https://app.saypi.ai 2 | API_SERVER_URL=https://api.saypi.ai 3 | AUTH_SERVER_URL=https://www.saypi.ai 4 | GA_MEASUREMENT_ID=G-N70M5T0ZCB 5 | GA_API_SECRET=4HsiqPmGS7Gs3TYWOa2ddg 6 | GA_ENDPOINT=https://www.google-analytics.com/mp/collect 7 | 8 | # JWT Configuration 9 | # AUTH_SESSION_LIFETIME=360 -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | 25 | - name: Install dependencies 26 | run: | 27 | npm cache clean --force 28 | # Try to use the existing lockfile first, if it fails then regenerate 29 | npm install --no-fund --prefer-offline || ( 30 | echo "Initial install failed, regenerating package-lock.json..." 31 | rm -f package-lock.json 32 | npm install --no-fund 33 | ) 34 | timeout-minutes: 10 35 | env: 36 | npm_config_fetch_retries: 5 37 | npm_config_fetch_retry_factor: 2 38 | npm_config_fetch_retry_mintimeout: 20000 39 | npm_config_fetch_retry_maxtimeout: 120000 40 | 41 | - name: Run tests 42 | run: npm test 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/* 3 | certificates/*.pem 4 | _locales_*_messages_json.js 5 | .DS_Store 6 | public/*.js 7 | public/*.js.LICENSE.txt 8 | public/*.js.map 9 | public/*.wasm 10 | src/audio/vad-configs.json 11 | src/audio/capabilities-test.json 12 | .DS_Store 13 | source-code.zip 14 | doc/dom/claude/* 15 | doc/data/* 16 | doc/store/* 17 | .env 18 | .env.* 19 | 20 | # Added by Claude Task Master 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | dev-debug.log 28 | # Dependency directories 29 | node_modules/ 30 | # Environment variables 31 | # Editor directories and files 32 | .idea 33 | .vscode 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | # OS specific 40 | # Task files 41 | tasks.json 42 | tasks/ 43 | public/offscreen/* 44 | public/permissions/* -------------------------------------------------------------------------------- /.roo/rules/roo_rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Guidelines for creating and maintaining Roo Code rules to ensure consistency and effectiveness. 3 | globs: .roo/rules/*.md 4 | alwaysApply: true 5 | --- 6 | 7 | - **Required Rule Structure:** 8 | ```markdown 9 | --- 10 | description: Clear, one-line description of what the rule enforces 11 | globs: path/to/files/*.ext, other/path/**/* 12 | alwaysApply: boolean 13 | --- 14 | 15 | - **Main Points in Bold** 16 | - Sub-points with details 17 | - Examples and explanations 18 | ``` 19 | 20 | - **File References:** 21 | - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files 22 | - Example: [prisma.md](mdc:.roo/rules/prisma.md) for rule references 23 | - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references 24 | 25 | - **Code Examples:** 26 | - Use language-specific code blocks 27 | ```typescript 28 | // ✅ DO: Show good examples 29 | const goodExample = true; 30 | 31 | // ❌ DON'T: Show anti-patterns 32 | const badExample = false; 33 | ``` 34 | 35 | - **Rule Content Guidelines:** 36 | - Start with high-level overview 37 | - Include specific, actionable requirements 38 | - Show examples of correct implementation 39 | - Reference existing code when possible 40 | - Keep rules DRY by referencing other rules 41 | 42 | - **Rule Maintenance:** 43 | - Update rules when new patterns emerge 44 | - Add examples from actual codebase 45 | - Remove outdated patterns 46 | - Cross-reference related rules 47 | 48 | - **Best Practices:** 49 | - Use bullet points for clarity 50 | - Keep descriptions concise 51 | - Include both DO and DON'T examples 52 | - Reference actual code over theoretical examples 53 | - Use consistent formatting across rules -------------------------------------------------------------------------------- /.taskmasterconfig: -------------------------------------------------------------------------------- 1 | { 2 | "models": { 3 | "main": { 4 | "provider": "anthropic", 5 | "modelId": "claude-3-7-sonnet-20250219", 6 | "maxTokens": 120000, 7 | "temperature": 0.2 8 | }, 9 | "research": { 10 | "provider": "perplexity", 11 | "modelId": "sonar-pro", 12 | "maxTokens": 8700, 13 | "temperature": 0.1 14 | }, 15 | "fallback": { 16 | "provider": "anthropic", 17 | "modelId": "claude-3.5-sonnet-20240620", 18 | "maxTokens": 120000, 19 | "temperature": 0.1 20 | } 21 | }, 22 | "global": { 23 | "logLevel": "info", 24 | "debug": false, 25 | "defaultSubtasks": 5, 26 | "defaultPriority": "medium", 27 | "projectName": "Taskmaster", 28 | "ollamaBaseUrl": "http://localhost:11434/api", 29 | "azureOpenaiBaseUrl": "https://your-endpoint.openai.azure.com/" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/i18n-ally-reviews.yml: -------------------------------------------------------------------------------- 1 | # Review comments generated by i18n-ally. Please commit this file. 2 | 3 | reviews: 4 | assistantIsListening.placeholders.chatbot.content: 5 | description: Placeholder argument. Do not translate. 6 | assistantIsListening.placeholders.chatbot.example: 7 | description: Product name, e.g. "Pi", "ChatGPT", "Grok", "Bard", etc. Do not translate. 8 | extensionPopupTitle.message: 9 | description: Title of the Say, Pi extension's popup dialog. Do not translate "Say, Pi". 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Python Debugger: Current File", 5 | "type": "debugpy", 6 | "request": "launch", 7 | "program": "${file}", 8 | "console": "integratedTerminal" 9 | }, 10 | { 11 | "type": "msedge", 12 | "name": "Hey Pi", 13 | "request": "launch", 14 | "url": "https://pi.ai/talk" 15 | }, 16 | { 17 | "type": "msedge", 18 | "name": "Say, Pi (update script)", 19 | "request": "launch", 20 | "url": "https://localhost:4443/saypi.user.js" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Say, Pi (application server)", 26 | "runtimeExecutable": "npm", 27 | "runtimeArgs": ["run-script", "start"] 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Vitest Tests", 33 | "runtimeExecutable": "npx", 34 | "runtimeArgs": ["vitest"] 35 | }, 36 | { 37 | "type": "node", 38 | "request": "launch", 39 | "name": "Vitest Tests (Debug)", 40 | "runtimeExecutable": "npx", 41 | "runtimeArgs": [ 42 | "vitest", 43 | "--inspect-brk", 44 | "test/dom/ChatHistoryObserver.spec.ts" 45 | ] 46 | }, 47 | { 48 | "type": "node", 49 | "request": "launch", 50 | "name": "Debug Specific Test", 51 | "runtimeExecutable": "npx", 52 | "runtimeArgs": [ 53 | "vitest", 54 | "run", 55 | "--inspect-brk", 56 | "test/dom/ChatHistoryObserver.spec.ts", 57 | "-t", 58 | "text and stable text should converge" 59 | ], 60 | "autoAttachChildProcesses": true, 61 | "console": "integratedTerminal" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["_locales"], 3 | "i18n-ally.keystyle": "nested", 4 | "CodeGPT.apiKey": "CodeGPT Plus Beta", 5 | "files.watcherExclude": { 6 | "**/target": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Proprietary Software License for Say, Pi (web frontend) 2 | 3 | Copyright (c) 2023, 2024, Ross Cadogan / Say, Pi 4 | 5 | All rights reserved. 6 | 7 | 1. Grant of License 8 | This license grants you the following rights: 9 | a. Usage: You may use this software for personal and non-commercial purposes. 10 | b. Inspection: You may inspect the source code for security and compatibility purposes. 11 | 12 | 2. Restrictions 13 | You are expressly prohibited from: 14 | a. Copying, modifying, merging, forking, or distributing the software for any purpose. 15 | b. Decompiling, reverse engineering, disassembling, or otherwise attempting to derive the source code for the software. 16 | c. Creating derivative works based on the software. 17 | d. Redistributing or sublicensing the software. 18 | 19 | 3. Termination 20 | This License is effective until terminated. It will terminate automatically without notice if you fail to comply with any of its terms and conditions. Upon termination, you must destroy all copies of the software. 21 | 22 | 4. No Warranty 23 | The software is provided "as is", without warranty of any kind, express or implied. 24 | 25 | 5. Limitation of Liability 26 | In no event shall the author or copyright holders be liable for any claim, damages, or other liability arising from the use or inability to use the software. 27 | 28 | 6. Public Visibility 29 | While the source code of this software may be publicly visible for transparency and security review purposes, this visibility does not grant any rights beyond those explicitly stated in this license. The restrictions outlined in Section 2 remain in full effect regardless of the code's visibility. 30 | 31 | Ross Cadogan / Say Pi, 32 | info@saypi.ai 33 | -------------------------------------------------------------------------------- /_locales/ja/description.txt: -------------------------------------------------------------------------------- 1 | Say, Piは、PiおよびClaudeとの会話をスムーズで音声駆動の体験に変えます。通話ボタンをタップし、直接人に話すように話しかけると、リアルな音声での返信が得られます。 2 | マルチタスキング、アクセシビリティ、または声に出して考える方に最適です。 3 | 4 | 無料プラン – 即座に試用、アカウント不要 5 | - タイピングの代わりに話す: Whisperの音声認識を使用してPiやClaudeと会話。あなたが話し終えたことを理解します。 6 | - 正確で高速: アクセントやバックグラウンドノイズがあっても高精度の音声からテキストへの変換をお楽しみください。 7 | - Piの本物の声を聞く: Piの組み込みのスタイルで自然な音声の返信。 8 | - 柔軟なレイアウト: フルスクリーンの没入モードまたは標準のチャットビューを使用できます。 9 | - ダークモード対応: Piのインターフェースに溶け込む目に優しいデザイン。 10 | - どこでも動作: Mac, Windows, Linux, Android, iOS – すべてに対応。 11 | 12 | Say, Piプレミアム – 完全な体験のアンロック 13 | あなたの声、あなたの言語、あなたのAI – すべてのデバイスで。 14 | - Claude & Piサポート: トップAIアシスタント間のシームレスな切り替え。 15 | - 32以上の言語でのリアルな返信: ElevenLabsによって、あなたの言語で自然な音になるように作られています。 16 | - 使用制限の増大: より多くの会話時間、より多くのリスニング時間。 17 | - 1つのアカウント、どこでも: プレミアムはプラットフォームやブラウザを超えてあなたをサポートします。 18 | 19 | 料金を見る: https://saypi.ai/pricing 20 | 21 | Say, Piが多くの人に愛される理由 22 | - ワークフローの向上: 手を離してノートを取ったり、アイデアを出したり、下書きしたり。 23 | - どんな言語でも話す: 会話中にスムーズに言語を切り替える多言語サポート。 24 | - アクセシビリティのために設計: インクルーシブなデザイン – 運動や視覚の課題を持つユーザーに最適。 25 | - 2000以上のユーザーから★4.9/5の評価: 拡大を続けるグローバルコミュニティから信頼されています。 26 | - プライバシー最優先: あなたの音声データは安全で、不適切に使用されません。ポリシーを確認: https://saypi.ai/legal/privacy 27 | 28 | 数秒で始める方法 29 | 1. 拡張機能をインストール。 30 | 2. pi.aiまたはclaude.aiを開く。 31 | 3. 通話ボタンをクリックして話す。それだけです。 32 | 33 | 自然な方法でAIと話し始めましょう – Say, Piをインストールして会話の未来を体験してください。 -------------------------------------------------------------------------------- /_locales/ko/description.txt: -------------------------------------------------------------------------------- 1 | Say, Pi는 Pi와 Claude와의 대화를 매끄럽고 음성 중심의 경험으로 전환합니다. 2 | 통화 버튼을 누르고 사람에게 말하듯이 이야기를 나누면 생생한 음성 응답이 돌아옵니다. 3 | 멀티태스킹, 접근성, 또는 소리를 내며 생각하는 것이 더 잘 맞는 사람들에게 적합합니다. 4 | 5 | 6 | 무료 플랜 – 즉시 사용 가능, 계정 필요 없음 7 | - 타이핑 대신 말하기: Whisper 기반의 음성 인식을 통해 Pi와 Claude와 대화할 수 있으며, 언제 말씀이 끝났는지 알고 있습니다. 8 | - 정확하고 빠름: 억양과 배경 소음에도 높은 정확도의 음성 인식을 즐기세요. 9 | - Pi의 실제 목소리를 들어보세요: Pi의 내장 스타일로 자연스러운 음성 응답을 제공합니다. 10 | - 유연한 레이아웃: 전체 화면 몰입 모드 또는 기본 채팅 뷰를 사용할 수 있습니다. 11 | - 다크 모드 지원: Pi의 인터페이스와 잘 어울리는 눈에 친화적인 디자인. 12 | - 어디서나 작동: Mac, Windows, Linux, Android, iOS – 모두 지원됩니다. 13 | 14 | 15 | Say, Pi 프리미엄 – 전체 경험을 잠금 해제하세요 16 | 당신의 목소리, 당신의 언어, 당신의 AI – 모든 기기에서 사용 가능합니다. 17 | - Claude 및 Pi 지원: 최고의 AI 어시스턴트 간 원활한 전환. 18 | - 32개 이상의 언어로 생생한 응답: ElevenLabs에 의해 자연스럽게 들리도록 제작되었습니다. 19 | - 더 큰 사용 한도: 더 많은 대화 시간, 더 많은 청취 시간. 20 | - 모든 곳에서 하나의 계정: 프리미엄은 플랫폼과 브라우저를 넘나들며 따라다닙니다. 21 | 22 | 가격 정보 보기: https://saypi.ai/pricing 23 | 24 | 25 | 수천 명이 Say, Pi를 사랑하는 이유 26 | - 워크플로우 강화: 손을 자유롭게 하며 노트 작성, 아이디어 구상, 초안 작성. 27 | - 모든 언어로 말하기: 중간 대화 전환이 매끄러운 다국어 지원. 28 | - 접근성을 염두에 두고 설계: 모터 또는 시각적 어려움이 있는 사용자에게 적합. 29 | - ★ 4.9/5 평점, 2,000명 이상의 사용자 평가: 성장하는 글로벌 커뮤니티가 신뢰합니다. 30 | - 개인 정보 우선: 귀하의 음성 데이터는 안전하며 악용되지 않습니다. 정책 보기: https://saypi.ai/legal/privacy 31 | 32 | 33 | 몇 초 만에 시작하세요 34 | 1. 확장 프로그램을 설치하세요. 35 | 2. pi.ai 또는 claude.ai를 엽니다. 36 | 3. 통화 버튼을 클릭하고 말하세요. 끝입니다. 37 | 38 | 39 | 자연스러운 방식으로 AI와 대화를 시작하세요—Say, Pi를 설치하고 대화의 미래를 경험해 보세요. -------------------------------------------------------------------------------- /_locales/zh_CN/description.txt: -------------------------------------------------------------------------------- 1 | Say, Pi 将您与 Pi 和 Claude 的对话转变为流畅的语音驱动体验。 2 | 只需点击通话按钮,就像与人说话那样进行交流,并获得逼真的语音回复。 3 | 非常适合多任务处理、无障碍访问,或任何更习惯于出声思考的人。 4 | 5 | 免费计划 - 立即体验,无需账户 6 | - 用说话代替打字:使用 Whisper 驱动的语音识别与 Pi 和 Claude 交流,该识别可以判断您何时说完。 7 | - 准确且快速:即使有口音和背景噪音,也能享受高准确度的语音到文字转换。 8 | - 听 Pi 的真实语音:Pi 内置风格的自然语音回复。 9 | - 灵活布局:使用全屏沉浸模式或标准聊天视图。 10 | - 支持暗模式:符合眼睛健康的设计,与 Pi 的界面融为一体。 11 | - 无处不在:Mac、Windows、Linux、Android、iOS 均可使用。 12 | 13 | Say, Pi Premium - 解锁完整体验 14 | 您的声音,您的语言,您的 AI - 跨越您所有的设备。 15 | - Claude & Pi 支持:在顶级 AI 助手之间无缝切换。 16 | - 32+ 语言的逼真回复:由 ElevenLabs 提供支持,精心打造以自然的方式呈现您的语言。 17 | - 更大使用限制:更多的通话时间和聆听时间。 18 | - 一个账户,到处使用:Premium 随您在平台和浏览器中移动。 19 | 20 | 查看定价:https://saypi.ai/pricing 21 | 22 | 为什么成千上万人喜爱 Say, Pi 23 | - 提升您的工作流程:在双手空闲时记笔记、构思或草拟。 24 | - 说任何语言:支持多语言,顺畅地进行中途切换。 25 | - 为无障碍设计:考虑包容性设计 - 非常适合有运动或视觉障碍的用户。 26 | - 用户评分 ★ 4.9/5 由 2000+ 用户提供:受到全球社区的信任。 27 | - 隐私优先:您的语音数据安全并绝不被滥用。查看我们的政策:https://saypi.ai/legal/privacy 28 | 29 | 几秒钟内开始 30 | 1. 安装扩展程序。 31 | 2. 打开 pi.ai 或 claude.ai。 32 | 3. 点击通话按钮并开始说话。就是这样。 33 | 34 | 以自然的方式开始与 AI 交流 - 安装 Say, Pi,体验对话的未来。 -------------------------------------------------------------------------------- /copy-onnx-files.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script copies required ONNX runtime files from node_modules to the public directory. 5 | * It should be run as part of the build process to ensure all necessary files are available. 6 | */ 7 | 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | import { fileURLToPath } from 'url'; 11 | 12 | // Get the directory name in ESM 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | // Define source and destination directories 17 | const sourceDir = path.join(__dirname, 'node_modules', 'onnxruntime-web', 'dist'); 18 | const destDir = path.join(__dirname, 'public'); 19 | 20 | // Ensure destination directory exists 21 | if (!fs.existsSync(destDir)) { 22 | fs.mkdirSync(destDir, { recursive: true }); 23 | } 24 | 25 | // Files to copy - both .wasm and .mjs files 26 | console.log('Copying ONNX runtime files...'); 27 | 28 | // Get all files from the source directory that match the patterns 29 | const onnxFiles = fs.readdirSync(sourceDir).filter( 30 | file => (file.startsWith('ort-wasm') && (file.endsWith('.wasm') || file.endsWith('.mjs'))) 31 | ); 32 | 33 | // Copy each file 34 | let copiedCount = 0; 35 | onnxFiles.forEach(file => { 36 | const sourcePath = path.join(sourceDir, file); 37 | const destPath = path.join(destDir, file); 38 | 39 | try { 40 | fs.copyFileSync(sourcePath, destPath); 41 | console.log(`✓ Copied ${file}`); 42 | copiedCount++; 43 | } catch (err) { 44 | console.error(`✗ Failed to copy ${file}: ${err.message}`); 45 | } 46 | }); 47 | 48 | console.log(`Finished copying ${copiedCount} ONNX runtime files.`); 49 | -------------------------------------------------------------------------------- /doc/Modules.md: -------------------------------------------------------------------------------- 1 | The architecture of "Say, Pi" takes a modular approach that aligns well with best practices for software development, making the application easier to maintain, test, and expand upon. Each module has a clear area of responsibility: 2 | 3 | 1. **saypi.index.js**: Acts as the entry point and orchestrator for the application. This is where everything gets set up, and the modules get loaded. 4 | 2. **EventModule.js**: Centralizes the event-handling logic. This is an excellent practice because it keeps the event-handling logic decoupled from the UI and other functionalities. It's also easier to add new events or change existing ones. 5 | 6 | 3. **ButtonModule.js**: Handles the UI logic related to buttons. Having a module dedicated to UI elements like buttons can help make the code more reusable and easier to manage. 7 | 8 | 4. **AnimationModule.js**: Dedicated to SVG animations, which is again a clean separation of concerns. Having a module just for animations means that you can add, modify, or remove animations without touching any other code. 9 | 10 | 5. **transcriber.js**: Handles the audio part of the application, including recording and playback. This is an important module to have in an application that relies on audio input and output. 11 | 12 | The event-driven architecture allows these modules to communicate without being tightly coupled, which is valuable for maintainability and testability. 13 | -------------------------------------------------------------------------------- /i18n-clear-keys.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Python script is used to clear the value of a specific key in the messages.json files 3 | in all locale directories. 4 | """ 5 | 6 | import os 7 | import json 8 | import sys 9 | 10 | # Get the key to be cleared from the command-line arguments 11 | if len(sys.argv) != 2: 12 | print("Usage: python i18n-clear-keys.py ") 13 | sys.exit(1) 14 | 15 | key_to_clear = sys.argv[1] 16 | 17 | # Get the current directory 18 | current_directory = os.getcwd() 19 | locales_directory = os.path.join(current_directory, "_locales") 20 | 21 | # For each locale directory in the locales directory 22 | for directory in os.listdir(locales_directory): 23 | locale_directory = os.path.join(locales_directory, directory) 24 | # If the directory name matches the locale pattern 25 | if os.path.isdir(locale_directory): 26 | # Open the messages.json file in that directory 27 | with open(os.path.join(locale_directory, "messages.json"), "r+") as file: 28 | # Load the JSON data 29 | data = json.load(file) 30 | 31 | # If the key is in the data, clear its value 32 | if key_to_clear in data: 33 | data[key_to_clear]["message"] = "" 34 | 35 | # Write the updated JSON data back to the file 36 | file.seek(0) 37 | json.dump(data, file, ensure_ascii=False, indent=2) 38 | file.truncate() 39 | -------------------------------------------------------------------------------- /i18n-delete-keys.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Python script is used to delete a set of unused keys from the messages.json files 3 | in all locale directories. 4 | """ 5 | 6 | import os 7 | import json 8 | 9 | # Define the keys to be removed 10 | keys_to_remove = ["exitMobileMode", "enterMobileMode"] 11 | 12 | # Get the current directory 13 | current_directory = os.getcwd() 14 | locales_directory = os.path.join(current_directory, "_locales") 15 | 16 | # For each locale directory in the locales directory 17 | for directory in os.listdir(locales_directory): 18 | locale_directory = os.path.join(locales_directory, directory) 19 | # If the directory name matches the locale pattern 20 | if os.path.isdir(locale_directory): 21 | # Open the messages.json file in that directory 22 | with open(os.path.join(locale_directory, "messages.json"), "r+") as file: 23 | # Load the JSON data 24 | data = json.load(file) 25 | 26 | # Remove the keys that are in the keys to be removed list 27 | for key in keys_to_remove: 28 | if key in data: 29 | del data[key] 30 | 31 | # Write the updated JSON data back to the file 32 | file.seek(0) 33 | json.dump(data, file, ensure_ascii=False, indent=2) 34 | file.truncate() 35 | -------------------------------------------------------------------------------- /ngrok.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | authtoken: 2sGPjQVQoEk4TlrerY6pChOFzGR_3M4RKCBEaLbTvJmCUyJdk 3 | tunnels: 4 | api: 5 | addr: https://localhost:5001 6 | proto: http 7 | inspect: false 8 | auth: 9 | addr: http://localhost:3000 10 | proto: http 11 | inspect: false -------------------------------------------------------------------------------- /public/6d076abcfd16e9f4ad0b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/6d076abcfd16e9f4ad0b.png -------------------------------------------------------------------------------- /public/audio/attention-1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/attention-1.mp3 -------------------------------------------------------------------------------- /public/audio/attention-2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/attention-2.mp3 -------------------------------------------------------------------------------- /public/audio/beep-off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/beep-off.mp3 -------------------------------------------------------------------------------- /public/audio/beep-on.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/beep-on.mp3 -------------------------------------------------------------------------------- /public/audio/call-failed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/call-failed.mp3 -------------------------------------------------------------------------------- /public/audio/send-round-long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/send-round-long.mp3 -------------------------------------------------------------------------------- /public/audio/send-round-short.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/send-round-short.mp3 -------------------------------------------------------------------------------- /public/audio/startup-synth.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/startup-synth.mp3 -------------------------------------------------------------------------------- /public/audio/switch-off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/switch-off.mp3 -------------------------------------------------------------------------------- /public/audio/switch-on.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/switch-on.mp3 -------------------------------------------------------------------------------- /public/audio/test-tone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/test-tone.mp3 -------------------------------------------------------------------------------- /public/audio/turn-off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/audio/turn-off.mp3 -------------------------------------------------------------------------------- /public/icons/logos/inflection.ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/icons/logos/inflection.ai.png -------------------------------------------------------------------------------- /public/icons/logos/saypi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/icons/logos/saypi.png -------------------------------------------------------------------------------- /public/icons/microphone-muted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /public/logos/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/logos/marquee.png -------------------------------------------------------------------------------- /public/silero_vad.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/silero_vad.onnx -------------------------------------------------------------------------------- /public/silero_vad_legacy.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/silero_vad_legacy.onnx -------------------------------------------------------------------------------- /public/silero_vad_v5.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/public/silero_vad_v5.onnx -------------------------------------------------------------------------------- /scripts/example_prd.txt: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | [Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] 4 | 5 | # Core Features 6 | [List and describe the main features of your product. For each feature, include: 7 | - What it does 8 | - Why it's important 9 | - How it works at a high level] 10 | 11 | # User Experience 12 | [Describe the user journey and experience. Include: 13 | - User personas 14 | - Key user flows 15 | - UI/UX considerations] 16 | 17 | 18 | # Technical Architecture 19 | [Outline the technical implementation details: 20 | - System components 21 | - Data models 22 | - APIs and integrations 23 | - Infrastructure requirements] 24 | 25 | # Development Roadmap 26 | [Break down the development process into phases: 27 | - MVP requirements 28 | - Future enhancements 29 | - Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] 30 | 31 | # Logical Dependency Chain 32 | [Define the logical order of development: 33 | - Which features need to be built first (foundation) 34 | - Getting as quickly as possible to something usable/visible front end that works 35 | - Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] 36 | 37 | # Risks and Mitigations 38 | [Identify potential risks and how they'll be addressed: 39 | - Technical challenges 40 | - Figuring out the MVP that we can build upon 41 | - Resource constraints] 42 | 43 | # Appendix 44 | [Include any additional information: 45 | - Research findings 46 | - Technical specifications] 47 | -------------------------------------------------------------------------------- /src/CacheBuster.ts: -------------------------------------------------------------------------------- 1 | class CacheBuster { 2 | private static readonly ATTEMPT_PARAM = "attempt"; 3 | 4 | /** 5 | * Adds or increments an 'attempt' parameter to the given URL for cache busting. 6 | * @param url The URL to modify 7 | * @returns A new URL with an updated 'attempt' parameter 8 | */ 9 | public static addCacheBuster(url: string): string { 10 | const urlObj = new URL(url); 11 | const currentAttempt = this.getAttempt(url); 12 | urlObj.searchParams.set( 13 | this.ATTEMPT_PARAM, 14 | (currentAttempt + 1).toString() 15 | ); 16 | return urlObj.toString(); 17 | } 18 | 19 | /** 20 | * Retrieves the value of the 'attempt' parameter from the given URL. 21 | * @param url The URL to check 22 | * @returns The numeric value of the 'attempt' parameter, or 0 if not present or invalid 23 | */ 24 | public static getAttempt(url: string): number { 25 | const urlObj = new URL(url); 26 | const attemptParam = urlObj.searchParams.get(this.ATTEMPT_PARAM); 27 | if (attemptParam === null) { 28 | return 0; 29 | } 30 | const parsedAttempt = parseInt(attemptParam, 10); 31 | return isNaN(parsedAttempt) ? 0 : parsedAttempt; 32 | } 33 | } 34 | 35 | export { CacheBuster }; 36 | -------------------------------------------------------------------------------- /src/ConfigModule.js: -------------------------------------------------------------------------------- 1 | export const config = { 2 | appServerUrl: process.env.APP_SERVER_URL, 3 | apiServerUrl: process.env.API_SERVER_URL, 4 | authServerUrl: process.env.AUTH_SERVER_URL, 5 | GA_MEASUREMENT_ID: process.env.GA_MEASUREMENT_ID, 6 | GA_API_SECRET: process.env.GA_API_SECRET, 7 | GA_ENDPOINT: process.env.GA_ENDPOINT, 8 | }; 9 | -------------------------------------------------------------------------------- /src/ImmersionServiceLite.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is a simplified version of the Immersion Service class, 3 | * with fewer dependencies and a more focused purpose, 4 | * for client who only need to check the state of the immersive view. 5 | */ 6 | export class ImmersionStateChecker { 7 | // this function determines whether the immersive view is currently active 8 | static isViewImmersive() { 9 | const element = document.documentElement; 10 | return element.classList.contains("immersive-view"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ResourceModule.ts: -------------------------------------------------------------------------------- 1 | import { config } from "./ConfigModule.js"; 2 | 3 | // used by browser extensions 4 | function getExtensionResourceUrl(filename: string) { 5 | const web_accessible_resources_dir = "public"; 6 | const filepath = web_accessible_resources_dir + "/" + filename; 7 | return chrome.runtime.getURL(filepath); 8 | } 9 | 10 | // used by userscripts 11 | function getAppServerResourceUrl(filename: string) { 12 | return `${config.appServerUrl}/${filename}`; 13 | } 14 | 15 | // cross-platform way to get a resource URL 16 | export function getResourceUrl(filename: string) { 17 | if ( 18 | typeof chrome !== "undefined" && 19 | typeof chrome.runtime === "object" && 20 | chrome.runtime.id 21 | ) { 22 | return getExtensionResourceUrl(filename); 23 | } else { 24 | return getAppServerResourceUrl(filename); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TextModule.ts: -------------------------------------------------------------------------------- 1 | export function replaceEllipsisWithSpace(text: string): string { 2 | return text.replace(/\.\.\. ([A-Z])/g, (match, p1) => ` ${p1.toLowerCase()}`); 3 | } 4 | 5 | export function shortenTranscript(transcript: string, limit: number): string { 6 | if (transcript.length > limit) { 7 | return `…${transcript.substring(transcript.length - limit + 1)}`; 8 | } 9 | return transcript; 10 | } -------------------------------------------------------------------------------- /src/TimerModule.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate the delay before submitting a message to Pi. 3 | * 4 | * @param timeUserStoppedSpeaking - The time the user stopped speaking. 5 | * @param probabilityFinished - The probability that the user has finished speaking. Expected to be between 0 and 1. 6 | * @param tempo - The tempo of the user's speech. Expected to be between 0 and 1. 7 | * @param maxDelay - The maximum delay. 8 | * @returns The calculated delay. 9 | */ 10 | export function calculateDelay( 11 | timeUserStoppedSpeaking: number, 12 | probabilityFinished: number, 13 | tempo: number, 14 | maxDelay: number 15 | ): number { 16 | // Get the current time (in milliseconds) 17 | const currentTime = new Date().getTime(); 18 | 19 | // Calculate the time elapsed since the user stopped speaking (in milliseconds) 20 | const timeElapsed = currentTime - timeUserStoppedSpeaking; 21 | 22 | // We invert the tempo because a faster speech (tempo approaching 1) should reduce the delay 23 | let tempoFactor = 1 - tempo; 24 | 25 | // Calculate the combined probability factor 26 | let combinedProbability = probabilityFinished * tempoFactor; 27 | 28 | // The combined factor influences the initial delay 29 | const initialDelay = combinedProbability * maxDelay; 30 | 31 | // Calculate the final delay after accounting for the time already elapsed 32 | const finalDelay = Math.max(initialDelay - timeElapsed, 0); 33 | return finalDelay; 34 | } 35 | -------------------------------------------------------------------------------- /src/UserAgentModule.ts: -------------------------------------------------------------------------------- 1 | export function isSafari(): boolean { 2 | return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 3 | } 4 | 5 | export function isFirefox(): boolean { 6 | return /Firefox/.test(navigator.userAgent); 7 | } 8 | 9 | export function isMobileDevice(): boolean { 10 | return ( 11 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 12 | navigator.userAgent 13 | ) || 14 | (typeof window.matchMedia === "function" && 15 | window.matchMedia("(max-width: 820px)").matches) 16 | ); 17 | } 18 | 19 | export function addUserAgentFlags(): void { 20 | const isFirefoxAndroid: boolean = 21 | /Firefox/.test(navigator.userAgent) && /Android/.test(navigator.userAgent); 22 | const element: HTMLElement = document.documentElement; 23 | 24 | if (isFirefoxAndroid) { 25 | element.classList.add("firefox-android"); 26 | } 27 | 28 | addDeviceFlags(element); 29 | //addViewFlags(element); // redundant, as this is called in initMode 30 | } 31 | 32 | export function addDeviceFlags(element: HTMLElement): void { 33 | if (isMobileDevice()) { 34 | element.classList.add("mobile-device"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WakeLockModule.ts: -------------------------------------------------------------------------------- 1 | // The wake lock sentinel. 2 | let wakeLock: WakeLockSentinel | null = null; 3 | 4 | // Function that attempts to request a screen wake lock. 5 | export const requestWakeLock = async () => { 6 | if (!('wakeLock' in navigator)) { 7 | console.log('Screen Wake Lock API not supported by this browser.'); 8 | return; 9 | } 10 | if (wakeLock === null || wakeLock.released) { 11 | try { 12 | wakeLock = await navigator.wakeLock.request('screen'); 13 | wakeLock?.addEventListener('release', () => { 14 | console.debug('Screen Wake Lock released'); 15 | }); 16 | console.debug('Screen Wake Lock acquired.'); 17 | } 18 | catch (err: DOMException | any) { 19 | if (err instanceof DOMException && err.name === 'NotAllowedError') { 20 | // Handle NotAllowedError - consider sending the state machine a battery-level error event 21 | console.error(`Not allowed to keep screen awake. Check battery level? ${err.name}, ${err.message}`); 22 | } else { 23 | console.error(`${err.name}, ${err.message}`); 24 | } 25 | } 26 | } 27 | }; 28 | 29 | export const releaseWakeLock = async () => { 30 | if (wakeLock !== null) { 31 | await wakeLock.release(); 32 | wakeLock = null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/__mocks__/ConfigModule.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | appServerUrl: "https://app.example.com", 3 | apiServerUrl: "https://api.example.com", 4 | GA_MEASUREMENT_ID: "GA_MEASUREMENT_ID", 5 | GA_API_SECRET: "GA_API_SECRET", 6 | GA_ENDPOINT: "GA_ENDPOINT", 7 | }; 8 | -------------------------------------------------------------------------------- /src/audio/AudioEncoder.ts: -------------------------------------------------------------------------------- 1 | import { encodeWAV } from "./WavEncoder"; 2 | 3 | /** 4 | * Convert a Float32Array of audio samples to a WAV array buffer 5 | * @param audioData - The audio samples 6 | * @returns - The audio in WAV format as an ArrayBuffer 7 | */ 8 | export function convertToWavBuffer(audioData: Float32Array): ArrayBuffer { 9 | const arrayBuffer = encodeWAV(audioData); 10 | return arrayBuffer; 11 | } 12 | 13 | /** 14 | * Convert a Float32Array of audio samples to a WAV Blob 15 | * @param audioData - The audio samples 16 | * @returns - The audio in WAV format 17 | */ 18 | export function convertToWavBlob(audioData: Float32Array): Blob { 19 | const arrayBuffer = convertToWavBuffer(audioData); 20 | return new Blob([arrayBuffer], { type: "audio/wav" }); 21 | } 22 | -------------------------------------------------------------------------------- /src/audio/AudioEvents.ts: -------------------------------------------------------------------------------- 1 | // payload for audio:reload event 2 | type ReloadAudioRequest = { 3 | bypassCache: boolean; 4 | playImmediately: boolean; 5 | }; 6 | 7 | export { ReloadAudioRequest }; 8 | -------------------------------------------------------------------------------- /src/audio/SlowResponseHandlerAdapter.js: -------------------------------------------------------------------------------- 1 | // Adapter to make SlowResponseHandler work with the XState actor interface 2 | export class SlowResponseHandlerAdapter { 3 | constructor(handler) { 4 | this.handler = handler; 5 | } 6 | 7 | send(eventType, detail = {}) { 8 | if (eventType === 'error') { 9 | // Create a synthetic event that mimics what SlowResponseHandler expects 10 | const syntheticEvent = { 11 | target: { 12 | currentSrc: detail.source, 13 | // Don't try to create a MediaError directly 14 | error: detail.error || { 15 | code: 4, // MEDIA_ERR_SRC_NOT_SUPPORTED 16 | message: detail.detail || "Error loading audio" 17 | } 18 | } 19 | }; 20 | this.handler.handleAudioError(syntheticEvent); 21 | } 22 | // Other event types can be mapped here if needed 23 | } 24 | } 25 | 26 | export default SlowResponseHandlerAdapter; -------------------------------------------------------------------------------- /src/chatbots/ChatbotIdentifier.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A lightweight module for identifying the current chatbot without circular dependencies. 3 | * This module doesn't import any specific chatbot implementations to avoid dependency cycles. 4 | */ 5 | export class ChatbotIdentifier { 6 | /** 7 | * Identifies which chatbot is being used based on the current URL 8 | * @returns The identifier string for the current chatbot 9 | */ 10 | static identifyChatbot(): string { 11 | if (window.location.hostname.includes("claude")) { 12 | return "claude"; 13 | } else { 14 | return "pi"; 15 | } 16 | } 17 | 18 | /** 19 | * Gets the app ID for the current chatbot without instantiating the chatbot 20 | * @returns The app ID to use in API calls 21 | */ 22 | static getAppId(): string { 23 | return this.identifyChatbot(); 24 | } 25 | 26 | /** 27 | * Checks if the current chatbot matches a specific type 28 | * @param type The chatbot type to check against 29 | * @returns True if the current chatbot matches the specified type 30 | */ 31 | static isChatbotType(type: string): boolean { 32 | return this.identifyChatbot() === type; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/chatbots/ChatbotService.ts: -------------------------------------------------------------------------------- 1 | import { Chatbot } from "./Chatbot"; 2 | import { ClaudeChatbot } from "./Claude"; 3 | import { PiAIChatbot } from "./Pi"; 4 | import { ChatbotIdentifier } from "./ChatbotIdentifier"; 5 | 6 | /** 7 | * This is the single place a concrete chatbot is created. 8 | * All other parts of the application should use this service to get a chatbot. 9 | */ 10 | export class ChatbotService { 11 | static async getChatbot(): Promise { 12 | // Use the ChatbotIdentifier to determine which chatbot to instantiate 13 | const chatbotType = ChatbotIdentifier.identifyChatbot(); 14 | 15 | switch (chatbotType) { 16 | case "claude": 17 | return new ClaudeChatbot(); 18 | case "pi": 19 | default: 20 | return new PiAIChatbot(); 21 | } 22 | } 23 | 24 | static async addChatbotFlags(): Promise { 25 | // Use the identifier directly for adding CSS classes 26 | // This avoids instantiating a full chatbot object just for the class name 27 | const chatbotType = ChatbotIdentifier.identifyChatbot(); 28 | document.body.classList.add(chatbotType); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/dom/BaseObserver.ts: -------------------------------------------------------------------------------- 1 | abstract class BaseObserver { 2 | protected observer: MutationObserver; 3 | protected target: Element | null; 4 | 5 | constructor(searchRoot: HTMLElement, protected selector: string) { 6 | if (searchRoot.matches(selector)) { 7 | this.target = searchRoot; 8 | } else { 9 | this.target = searchRoot.querySelector(selector); 10 | } 11 | 12 | this.observer = new MutationObserver(this.callback.bind(this)); 13 | 14 | if (!this.target) { 15 | console.warn(`Element with selector ${selector} not found.`); 16 | } 17 | } 18 | 19 | protected abstract callback( 20 | mutations: MutationRecord[], 21 | observer: MutationObserver 22 | ): void; 23 | 24 | public observe(options: MutationObserverInit): void { 25 | if (this.target) { 26 | this.observer.observe(this.target, options); 27 | } 28 | } 29 | 30 | public disconnect(): void { 31 | this.observer.disconnect(); 32 | } 33 | } 34 | 35 | export { BaseObserver }; 36 | -------------------------------------------------------------------------------- /src/dom/MessageEvents.ts: -------------------------------------------------------------------------------- 1 | // src/dom/AssistantWritingEvent.ts 2 | 3 | import { SpeechUtterance } from "../tts/SpeechModel"; 4 | 5 | type AssistantWritingEvent = { 6 | utterance: SpeechUtterance; 7 | }; 8 | 9 | export { AssistantWritingEvent }; 10 | -------------------------------------------------------------------------------- /src/error-management/TranscriptionErrorManager.ts: -------------------------------------------------------------------------------- 1 | const ATTEMPT_WINDOW_SIZE = 6; 2 | const FAILURE_RATE_THRESHOLD = 0.5; // 50% 3 | 4 | class TranscriptionErrorManager { 5 | private static instance: TranscriptionErrorManager; 6 | private attempts: boolean[] = []; 7 | 8 | private constructor() {} 9 | 10 | public static getInstance(): TranscriptionErrorManager { 11 | if (!TranscriptionErrorManager.instance) { 12 | TranscriptionErrorManager.instance = new TranscriptionErrorManager(); 13 | } 14 | return TranscriptionErrorManager.instance; 15 | } 16 | 17 | public recordAttempt(success: boolean): void { 18 | this.attempts.push(success); 19 | if (this.attempts.length > ATTEMPT_WINDOW_SIZE) { 20 | this.attempts.shift(); // Keep the window size 21 | } 22 | } 23 | 24 | public getFailureRate(): number { 25 | if (this.attempts.length === 0) { 26 | return 0; 27 | } 28 | const failures = this.attempts.filter(attempt => !attempt).length; 29 | return failures / this.attempts.length; 30 | } 31 | 32 | public shouldShowUserHint(): boolean { 33 | // Only show hint if there are at least a few attempts to base the rate on 34 | if (this.attempts.length < ATTEMPT_WINDOW_SIZE / 2) { 35 | // Avoid showing the hint too eagerly on initial sparse failures. 36 | // e.g. if window is 10, don't show until at least 5 attempts. 37 | return false; 38 | } 39 | return this.getFailureRate() > FAILURE_RATE_THRESHOLD; 40 | } 41 | 42 | public getAttemptsWindow(): readonly boolean[] { 43 | return this.attempts; 44 | } 45 | 46 | public reset(): void { 47 | this.attempts = []; 48 | } 49 | } 50 | 51 | export default TranscriptionErrorManager.getInstance(); -------------------------------------------------------------------------------- /src/events/EventBus.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | 3 | export default new EventEmitter(); 4 | -------------------------------------------------------------------------------- /src/icons/bubble-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/icons/bubble-128px.png -------------------------------------------------------------------------------- /src/icons/bubble-16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/icons/bubble-16px.png -------------------------------------------------------------------------------- /src/icons/bubble-300px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/icons/bubble-300px.png -------------------------------------------------------------------------------- /src/icons/bubble-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/icons/bubble-32px.png -------------------------------------------------------------------------------- /src/icons/bubble-48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/icons/bubble-48px.png -------------------------------------------------------------------------------- /src/icons/claude-chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/copied.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/icons/flags/ae.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/ag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/am.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/ao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/at.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/au.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/ax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/icons/flags/az.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/ba.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/bb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/bd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/be.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/bf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/bh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/bi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/bj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/bl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/bq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/bs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/bv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/bw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/ca.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/cd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/cefta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/cf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/cg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/ch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/ci.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/cl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/cm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/cn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/flags/co.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/cp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/cr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/cu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/cv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/cw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/cz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/de.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/dj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/dk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/dz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/ee.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/eh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/icons/flags/es-ct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/es-pv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/et.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/eu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/icons/flags/fi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/fm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/flags/fo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/fr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/ga.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/gb-eng.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/gb-sct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/gb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/gd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/icons/flags/ge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/gf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/gg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/gh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/gl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/gm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/gn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/gp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/gr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/icons/flags/gw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/gy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/hk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/hm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/hn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/icons/flags/hu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/ic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/id.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/ie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/il.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/icons/flags/iq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/icons/flags/is.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/it.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/jm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/jo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/icons/flags/jp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/flags/ke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/icons/flags/km.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/icons/flags/kn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/kp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/kr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/icons/flags/kw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/la.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/lc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/lr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/ls.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/lt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/lu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/lv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/ly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/ma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/mc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/mf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/mg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/mh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/mk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/ml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/mm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/mn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/mo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/mq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/mr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/mu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/mv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/my.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/icons/flags/na.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/icons/flags/nc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/ne.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/ng.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/nl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/np.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/nr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/flags/nu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/icons/flags/pa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/pe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/pg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/ph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/pk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/pl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/pm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/pr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/ps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/pw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/flags/qa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/re.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/ro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/ru.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/rw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/sb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/sc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/sd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/se.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/sg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/sh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/sj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/sk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/sl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/sn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/so.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/flags/sr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/ss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/st.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/icons/flags/sy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/td.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/tf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/flags/tg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/flags/th.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/tk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/tl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/tn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/to.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/icons/flags/tr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/tt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/tv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/tz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/flags/ua.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/flags/um.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/us.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/flags/uz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/icons/flags/vc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/flags/ve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/icons/flags/vn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/icons/flags/wf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/ws.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/xx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/flags/ye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/flags/yt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/flags/za.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/icons/immersive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /src/icons/maximize.svg: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | 13 | -------------------------------------------------------------------------------- /src/icons/regenerate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/unlock.svg: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /src/metadata.txt: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Say, Pi 3 | // @name:zh-CN 说,Pi 4 | // @namespace http://www.saypi.ai/ 5 | // @version 1.7.0 6 | // @description Seamless speech-to-text enhancement for Pi, the conversational AI. Enjoy hands-free, high-accuracy conversations in any language. 7 | // @description:zh-CN 为Pi聊天机器人提供无手操作的高精度语音转文字功能,支持多种语言。 8 | // @author Ross Cadogan 9 | // @match https://pi.ai/talk 10 | // @inject-into page 11 | // @updateURL https://app.saypi.ai/saypi.user.js 12 | // @downloadURL https://app.saypi.ai/saypi.user.js 13 | // @license MIT 14 | // ==/UserScript== 15 | -------------------------------------------------------------------------------- /src/offscreen/media_offscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SayPi Media Offscreen Document 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/offscreen/media_offscreen.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../LoggingModule.js"; 2 | import { setupMessageListener } from "./media_coordinator"; 3 | 4 | logger.log("[SayPi Media Offscreen] Script loaded."); 5 | 6 | // Set up the global message listener that will route messages to the appropriate handlers 7 | setupMessageListener(); 8 | 9 | logger.log("[SayPi Media Offscreen] Message listener initialized."); -------------------------------------------------------------------------------- /src/permissions/himfloyd-mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/permissions/himfloyd-mic.png -------------------------------------------------------------------------------- /src/permissions/permissions-prompt.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | padding: 20px; 4 | text-align: center; 5 | margin: 0; 6 | /* background-color: #f4f4f4; */ /* Replaced by background image */ 7 | color: #333; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | height: 100vh; 13 | background-image: url('himfloyd-mic.png'); 14 | background-size: cover; 15 | background-position: center; 16 | background-repeat: no-repeat; 17 | } 18 | 19 | .container { 20 | /* background-color: #fff; */ /* Made slightly transparent */ 21 | background-color: rgba(255, 255, 255, 0.9); 22 | padding: 30px; 23 | border-radius: 12px; 24 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08); 25 | max-width: 400px; 26 | } 27 | 28 | h1 { 29 | color: #2c3e50; 30 | margin-top: 0; 31 | } 32 | 33 | p { 34 | margin-bottom: 15px; 35 | line-height: 1.6; 36 | } 37 | 38 | #status { 39 | margin-top: 20px; 40 | font-weight: bold; 41 | } 42 | 43 | .footer-text { 44 | font-size: 0.9em; 45 | color: #777; 46 | margin-top: 20px; 47 | } -------------------------------------------------------------------------------- /src/permissions/permissions-prompt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microphone Permission 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

🗣️ Let's get chatting!

15 |

To bring voice to your conversations, Say, Pi needs permission to use your microphone. Your privacy matters - we only listen when you're actively chatting.

16 |

You'll see a browser prompt at the top of this window. Just hit "Allow while visiting the site" so we can start talking.

17 |

Waiting for your go-ahead…

18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /src/popup/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/popup/consent.css: -------------------------------------------------------------------------------- 1 | /* consent */ 2 | #analytics-consent { 3 | width: 580px; 4 | padding: 200px 1.5rem 1.5rem 1.5rem; 5 | background-image: url("data-sharing.png"); 6 | background-size: contain; 7 | background-repeat: no-repeat; 8 | } 9 | /* Mobile */ 10 | .mobile-device #analytics-consent { 11 | width: 400px; 12 | } 13 | #analytics-consent.read { 14 | display: none; 15 | } 16 | #analytics-consent h1 { 17 | margin-bottom: 1rem; 18 | } 19 | #analytics-consent p { 20 | margin-bottom: 1rem; 21 | } 22 | #analytics-consent ul { 23 | padding: 0 1rem; 24 | list-style-type: disc; 25 | } 26 | #analytics-consent ul li { 27 | padding-bottom: 0.5rem; 28 | } 29 | #analytics-consent a { 30 | display: block; 31 | margin-bottom: 1rem; 32 | text-decoration: underline; 33 | } 34 | #analytics-consent h2 { 35 | margin-bottom: 1rem; 36 | text-align: center; 37 | } 38 | #analytics-consent .buttons { 39 | display: flex; 40 | justify-content: space-between; 41 | margin-top: 1rem; 42 | padding: 0 1.5rem; 43 | } 44 | #analytics-consent .buttons button { 45 | width: 100px; 46 | border: 2px solid lightskyblue; 47 | padding: 0.5rem; 48 | border-radius: 10px; 49 | margin: 0 auto; 50 | } 51 | #analytics-consent .buttons button:hover { 52 | background-color: lightskyblue; 53 | color: white; 54 | } -------------------------------------------------------------------------------- /src/popup/data-sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pedal-Intelligence/saypi-userscript/ea4ae44eba3f405e1f22f05ebaef330e719a3ece/src/popup/data-sharing.png -------------------------------------------------------------------------------- /src/popup/popup-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto-generated from .env.production - DO NOT MODIFY DIRECTLY 3 | * This file is regenerated on each build to ensure it uses the correct environment settings. 4 | * Generated on: 2025-05-25 5 | */ 6 | const config = { 7 | // Values from .env.production 8 | apiBaseUrl: "https://api.saypi.ai", 9 | authServerUrl: "https://www.saypi.ai" 10 | }; 11 | 12 | // Config is now globally accessible via window.config 13 | // No export needed in popup context 14 | -------------------------------------------------------------------------------- /src/popup/popupopener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Opens the extension's settings popup by sending a message to the background script. 3 | * The background script will handle opening the popup in the native way. 4 | */ 5 | export function openSettings(): void { 6 | chrome.runtime.sendMessage({ action: 'openPopup' }); 7 | } -------------------------------------------------------------------------------- /src/popup/preferences.css: -------------------------------------------------------------------------------- 1 | /* Slider */ 2 | #sliderValue, #submitModeValue { 3 | display: block; 4 | text-align: center; 5 | } 6 | .icon { 7 | transition: transform 0.3s ease; 8 | padding: 0 0.5rem; 9 | } 10 | .icon.active { 11 | transform: scale(1.5); 12 | } 13 | #preferences .mode-selector { 14 | padding: 1rem; 15 | display: block; 16 | position: relative; 17 | } 18 | #language-preference { 19 | display: block; 20 | } 21 | 22 | /* Descriptions */ 23 | .description { 24 | display: none; 25 | } 26 | .user-preference-item:hover .description, 27 | .user-preference-item:focus-within .description { 28 | display: none; 29 | } 30 | /* hide the slider description unless it describes the selected preset */ 31 | .mode-selector .description { 32 | display: none; 33 | } 34 | .mode-selector.user-preference-item:hover .description.selected, 35 | .mode-selector.user-preference-item:focus-within .description.selected { 36 | display: block; 37 | text-align: center; 38 | margin-top: 0.5rem; 39 | width: 100%; 40 | color: #4a5568; 41 | } 42 | -------------------------------------------------------------------------------- /src/popup/simple-user-agent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module is a simplified version of the user agent module. It only checks if the user is on a mobile device or not. 3 | * Media queries do not work reliably on the action button popup, so we need to use JavaScript to check if the user is on a mobile device. 4 | */ 5 | 6 | /** 7 | * Basic check to see if the user is on a mobile device 8 | * Does not reliably check for tablets, only for phones 9 | * @returns {boolean} true if the user is on a mobile device, false otherwise 10 | 11 | */ 12 | function isMobileDevice() { 13 | return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 14 | navigator.userAgent 15 | ); 16 | } 17 | function addDeviceFlags(element) { 18 | if (isMobileDevice()) { 19 | element.classList.add("mobile-device"); 20 | } 21 | } 22 | addDeviceFlags(document.documentElement); 23 | -------------------------------------------------------------------------------- /src/popup/toggle.css: -------------------------------------------------------------------------------- 1 | /* Toggle */ 2 | .wraper { 3 | display: grid; 4 | justify-content: space-between; 5 | width: 100%; 6 | grid-template-columns: auto auto; 7 | gap: 1em; 8 | align-items: center; 9 | } 10 | 11 | .label-text { 12 | font-weight: normal; 13 | font-size: 1rem; 14 | display:inline-block; 15 | padding-right: 2em; 16 | } 17 | 18 | .switch-wrap { 19 | cursor: pointer; 20 | background: lightgray; 21 | padding: 2px; 22 | width: 30px; 23 | height: 17px; 24 | border-radius: 8.5px; 25 | } 26 | .switch-wrap.checked { 27 | background: rgb(11, 87, 208); 28 | } 29 | 30 | .switch-wrap input { 31 | position: absolute; 32 | opacity: 0; 33 | width: 0; 34 | height: 0; 35 | } 36 | 37 | .switch { 38 | height: 100%; 39 | display: grid; 40 | grid-template-columns: 0fr 1fr 1fr; 41 | transition: 0.2s; 42 | } 43 | .switch::after { 44 | content: ""; 45 | border-radius: 50%; 46 | background: #fff; 47 | grid-column: 2; 48 | transition: background 0.2s; 49 | } 50 | 51 | input:checked + .switch { 52 | grid-template-columns: 1fr 1fr 0fr; 53 | } 54 | input:checked + .switch::after { 55 | background-color: #fff; 56 | } -------------------------------------------------------------------------------- /src/popup/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | #saypi-callButton.disabled svg path.circle { 4 | fill: rgb(245 238 223); /* bg-cream-550 */ 5 | } 6 | 7 | 8 | .saypi-enter-button, 9 | .saypi-exit-button, 10 | .settings-button.mini { 11 | width: 3rem; 12 | height: 3rem; 13 | padding: 6px; 14 | border: 0; 15 | z-index: 60; 16 | svg { 17 | path.outer { 18 | fill: rgb(237 225 209); /* bg-cream-550 */ 19 | } 20 | path.inner { 21 | color: rgb(13 60 38); /* text-primary-700 */ 22 | } 23 | circle.outer-circle-bg { 24 | fill: rgb(237 225 209); /* bg-cream-550 */ 25 | } 26 | } 27 | } 28 | .settings-button.maxi svg circle.outer-circle-bg { 29 | /* make the settings button transparent in side panel */ 30 | display: none; 31 | } 32 | 33 | /* hide the control panel buttons on non-chat pages */ 34 | .saypi-control-panel.overflow-hidden { 35 | .saypi-control-button { 36 | display: none; 37 | } 38 | } 39 | 40 | 41 | #saypi-lock-panel { 42 | /* lock panel is only displayed on mobile devices */ 43 | display: none; 44 | } 45 | 46 | @import "progress-ring.scss"; 47 | @import "neon.scss"; 48 | @import "notifications.scss"; 49 | @import "voices.scss"; -------------------------------------------------------------------------------- /src/styles/focus-mode.scss: -------------------------------------------------------------------------------- 1 | html.immersive-view:not(.mobile-device) body.focus button { 2 | opacity: 0; 3 | transition: opacity 0.5s ease-out; 4 | } 5 | html.immersive-view button.settings-button.mini svg circle.outer-circle-bg { 6 | /* make the settings button transparent in focus mode */ 7 | display: none; 8 | } -------------------------------------------------------------------------------- /src/styles/pi.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains styles specific to Pi. 3 | */ 4 | 5 | html body.pi { 6 | .saypi-credit-notification { 7 | color: rgb(107, 98, 85); 8 | } 9 | } -------------------------------------------------------------------------------- /src/styles/progress-ring.scss: -------------------------------------------------------------------------------- 1 | @keyframes fillup { 2 | to { 3 | stroke-dashoffset: 0; 4 | } 5 | } 6 | 7 | @keyframes changeColor { 8 | 0% { stroke: green; } 9 | 50% { stroke: yellow; } 10 | 100% { stroke: red; } 11 | } 12 | 13 | #progress-ring { 14 | transform: rotate(-90deg); 15 | transform-origin: 50% 50%; 16 | } 17 | 18 | #progress-ring.active { 19 | animation: fillup 10s linear forwards, changeColor 10s linear forwards; 20 | } -------------------------------------------------------------------------------- /src/styles/voices.scss: -------------------------------------------------------------------------------- 1 | #saypi-voice-menu .saypi-voice-button { 2 | display: none; 3 | } 4 | #saypi-voice-menu.expanded .saypi-voice-button { 5 | display: block; 6 | } 7 | .saypi-voice-button img.flair { 8 | display: inline; 9 | height: 20px; 10 | padding-left: 0.5rem; 11 | } 12 | #saypi-voice-settings button.selected.paola { 13 | background-color: rgb(200, 200, 200); 14 | border-color: rgb(200, 200, 200); 15 | } 16 | #saypi-voice-settings button.selected.joey { 17 | background-color: rgb(255, 255, 200); 18 | border-color: rgb(255, 255, 200); 19 | } 20 | #saypi-voice-menu-controls.saypi-provided-voice { 21 | //background-color: #418a2f; // need a button color that indicates the voice is provided by SayPi, but not too jarring 22 | button { 23 | background-color: inherit; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/tts/FailedSpeechUtterance.ts: -------------------------------------------------------------------------------- 1 | import { SpeechUtterance, SpeechSynthesisVoiceRemote, audioProviders } from "./SpeechModel"; 2 | import { SpeechFailureReason } from "./SpeechFailureReason"; 3 | 4 | export class FailedSpeechUtterance implements SpeechUtterance { 5 | readonly id: string; 6 | readonly lang: string; 7 | readonly voice: SpeechSynthesisVoiceRemote; 8 | /** no usable audio URL */ 9 | readonly uri: string = ""; 10 | readonly provider = audioProviders.SayPi; 11 | readonly kind = "failed" as const; 12 | 13 | constructor( 14 | uuid: string, 15 | lang: string, 16 | voice: SpeechSynthesisVoiceRemote, 17 | public readonly reason: SpeechFailureReason 18 | ) { 19 | this.id = uuid; 20 | this.lang = lang; 21 | this.voice = voice; 22 | } 23 | 24 | toString() { 25 | return `[failed utterance: ${this.reason}]`; 26 | } 27 | } -------------------------------------------------------------------------------- /src/tts/SpeechFailureReason.ts: -------------------------------------------------------------------------------- 1 | export enum SpeechFailureReason { 2 | InsufficientCredit = "INSUFFICIENT_CREDIT", 3 | RateLimited = "RATE_LIMITED", 4 | Unknown = "UNKNOWN" 5 | } -------------------------------------------------------------------------------- /src/tts/__mocks__/SpeechSynthesisModule.ts: -------------------------------------------------------------------------------- 1 | // src/tts/__mocks__/SpeechSynthesisModule.ts 2 | export class SpeechSynthesisModule { 3 | getVoice = (voiceId: string) => { 4 | return { 5 | id: voiceId, 6 | name: "Samantha", 7 | lang: "en", 8 | localService: false, 9 | default: false, 10 | price: 0.3, 11 | powered_by: "saypi.ai", 12 | voiceURI: "", 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/tts/__mocks__/voice-settings.html: -------------------------------------------------------------------------------- 1 |
2 | 38 |
39 | -------------------------------------------------------------------------------- /src/vad/custom-model-fetcher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const CustomModelFetcher = async (path) => { 3 | let arrayBuf = await fetchModel(path); 4 | return copyArrayBuffer(arrayBuf); 5 | }; 6 | const fetchModel = (path) => { 7 | return fetch(path).then((model) => model.arrayBuffer()); 8 | }; 9 | const copyArrayBuffer = (original) => { 10 | var arrayBuffer = new ArrayBuffer(original.byteLength); 11 | new Uint8Array(arrayBuffer).set(new Uint8Array(original)); 12 | return arrayBuffer; 13 | }; 14 | export const customModelFetcher = CustomModelFetcher; -------------------------------------------------------------------------------- /test/dom/DOMModule.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from "vitest"; 2 | import { createSVGElement } from "../../src/dom/DOMModule"; 3 | import { setupTestDOM } from "../utils/dom"; 4 | 5 | describe("createSVGElement", () => { 6 | beforeEach(() => { 7 | setupTestDOM(); 8 | }); 9 | 10 | it("should create an SVG element from valid SVG string", () => { 11 | const svgString = 12 | ''; 13 | const svgElement = createSVGElement(svgString); 14 | 15 | expect(svgElement.tagName.toLowerCase()).toBe("svg"); 16 | expect(svgElement.getAttribute("width")).toBe("24"); 17 | expect(svgElement.getAttribute("height")).toBe("24"); 18 | }); 19 | 20 | it("should throw error for invalid SVG string", () => { 21 | const invalidSvgString = "Invalid"; 22 | 23 | expect(() => createSVGElement(invalidSvgString)).toThrow( 24 | "Failed to create SVGElement" 25 | ); 26 | }); 27 | 28 | it("should throw error for malformed SVG string", () => { 29 | const malformedSvgString = ""; 30 | 31 | expect(() => createSVGElement(malformedSvgString)).toThrow( 32 | "Failed to create SVGElement" 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/jest.setup.js: -------------------------------------------------------------------------------- 1 | import { TextEncoder, TextDecoder } from "util"; 2 | 3 | // Mock environment variables 4 | process.env.GA_MEASUREMENT_ID = "mock-measurement-id"; 5 | process.env.GA_API_SECRET = "mock-api-secret"; 6 | process.env.GA_ENDPOINT = "https://www.google-analytics.com/mp/collect"; 7 | 8 | // text-encoding is not supported in JSDOM, so we need to polyfill it 9 | global.TextEncoder = TextEncoder; 10 | global.TextDecoder = TextDecoder; 11 | 12 | // Mock `chrome` APIs for Jest 13 | if (typeof chrome === "undefined") { 14 | const storage = {}; 15 | global.chrome = { 16 | storage: { 17 | sync: { 18 | get: (keys, callback) => { 19 | // Mock implementation with in-memory storage 20 | return storage[keys]; 21 | }, 22 | set: (items, callback) => { 23 | // Mock implementation with in-memory storage 24 | storage = { ...storage, ...items }; 25 | return callback(); 26 | }, 27 | }, 28 | }, 29 | runtime: { 30 | onMessage: { 31 | addListener: () => {}, 32 | }, 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /test/merge.test.ts: -------------------------------------------------------------------------------- 1 | import { TranscriptMergeService } from "../src/TranscriptMergeService"; 2 | import { expect, describe, it } from "@jest/globals"; 3 | 4 | // Create an instance of the service with mock values for the constructor (doesn't make any network requests) 5 | const transcriptMergeService = new TranscriptMergeService( 6 | "http://mock-api-url", 7 | "en-US" 8 | ); 9 | 10 | describe("TranscriptMergeService.sortTranscripts", () => { 11 | it("should trim whitespace from transcripts", () => { 12 | const transcripts: Record = { 13 | 1: " first ", 14 | 2: "second ", 15 | 3: " third", 16 | }; 17 | const sorted = transcriptMergeService.sortTranscripts(transcripts); 18 | expect(sorted).toEqual(["first", "second", "third"]); 19 | }); 20 | 21 | it("should handle an empty list of transcripts", () => { 22 | const transcripts: Record = {}; 23 | const sorted = transcriptMergeService.sortTranscripts(transcripts); 24 | expect(sorted).toEqual([]); 25 | }); 26 | 27 | it("should correctly handle a list of transcripts that is already sorted", () => { 28 | const transcripts: Record = { 29 | 1: "first", 30 | 2: "second", 31 | 3: "third", 32 | }; 33 | const sorted = transcriptMergeService.sortTranscripts(transcripts); 34 | expect(sorted).toEqual(["first", "second", "third"]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/prefs/PreferenceModule.mock.ts: -------------------------------------------------------------------------------- 1 | import { Preference, VoicePreference } from "../../src/prefs/PreferenceModule"; 2 | import { SpeechSynthesisVoiceRemote } from "../../src/tts/SpeechModel"; 3 | import { vi } from "vitest"; 4 | import { mockVoices } from "../data/Voices"; 5 | 6 | export class UserPreferenceModuleMock { 7 | public static getInstance = vi.fn(() => new UserPreferenceModuleMock()); 8 | 9 | public getTranscriptionMode = vi.fn(() => 10 | Promise.resolve("balanced" as Preference) 11 | ); 12 | public getSoundEffects = vi.fn(() => Promise.resolve(true)); 13 | public getAutoSubmit = vi.fn(() => Promise.resolve(true)); 14 | public getLanguage = vi.fn(() => Promise.resolve("en-US")); 15 | public getTheme = vi.fn(() => Promise.resolve("light")); 16 | public setTheme = vi.fn(() => Promise.resolve()); 17 | public getDataSharing = vi.fn(() => Promise.resolve(false)); 18 | public getPrefersImmersiveView = vi.fn(() => Promise.resolve(false)); 19 | public hasVoice = vi.fn(() => Promise.resolve(true)); 20 | public getVoice = vi.fn(() => 21 | Promise.resolve(mockVoices[0] as SpeechSynthesisVoiceRemote) 22 | ); 23 | public setVoice = vi.fn(() => Promise.resolve()); 24 | public unsetVoice = vi.fn(() => Promise.resolve()); 25 | 26 | private getStoredValue(key: string, defaultValue: any): Promise { 27 | return new Promise((resolve) => { 28 | resolve(defaultValue); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom"; 2 | 3 | export function setupTestDOM() { 4 | const dom = new JSDOM('', { 5 | url: 'http://localhost', 6 | pretendToBeVisual: true, 7 | }); 8 | 9 | // Clear and update the existing document 10 | document.documentElement.innerHTML = dom.window.document.documentElement.innerHTML; 11 | 12 | // Add any missing properties to the existing document 13 | if (!global.SVGElement) { 14 | global.SVGElement = dom.window.SVGElement; 15 | } 16 | if (!global.DOMParser) { 17 | global.DOMParser = dom.window.DOMParser; 18 | } 19 | 20 | return document; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "outDir": "./public", 6 | "target": "ES6", 7 | "module": "ESNext", // Changed to ESNext for ES modules 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.js", "src/*.d.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | include: ["**/*.spec.ts"], 4 | globals: true, 5 | setupFiles: ["test/vitest.setup.js"], 6 | }, 7 | testTimeout: 10000, 8 | }; 9 | --------------------------------------------------------------------------------