Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
우리 AI 리셉션리스트가 웹사이트 방문자와 대화하고 질문에 답변하며 정보를 수집합니다 24/7
Our AI receptionist talks to your website visitors, answers their questions, and captures their details, 24/7. Preview it for your own business in under a minute.
비즈니스JavaScript · HTMLGitHub @justanothervisionary온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A single-script-tag AI chat widget for local business websites - answers
common patient/customer questions (hours, services, booking, location),
captures leads it can't answer, hands off anything urgent to a phone call.
Sold direct as a monthly subscription - no marketplace cut.
First target: West London Dental Centres (dentistw4.co.uk), Chiswick.
Future idea: a premium tier that takes payment in-chat (not built yet)README 전문 보기 ↗A single-script-tag AI chat widget for local business websites - answers common patient/customer questions (hours, services, booking, location), captures leads it can't answer, hands off anything urgent to a phone call. Sold direct as a monthly subscription - no marketplace cut.
First target: West London Dental Centres (dentistw4.co.uk), Chiswick.
Instead of just capturing a lead for the business to follow up on, Sia closes the transaction herself - customer books and pays right in the widget. Noted here so it doesn't get lost, deliberately not scoped for now - it's a materially bigger build than anything else in this project:
Right sequencing: after client #1 is real and paying, not before. Also a legitimate premium-tier price justification once it exists - "closes sales while you sleep" is worth noticeably more than £45/mo.
Once there are real paying clients, it'd be useful for Claude to answer things like "how many are actually paying right now," "did anyone's payment fail this month," "what's MRR" - direct reporting instead of digging through the Stripe dashboard by hand.
How, when we get there: a Stripe Restricted API key, scoped to read-only on Payment Links / Subscriptions / Charges specifically. No refund, payout, transfer, or account-settings permissions on that key - ever. Stored the same way as the other secrets (env var, never pasted loosely in chat).
Hard line, not just a preference: anything write/mutating - issuing a refund, cancelling a client's subscription, changing payout details, creating new pricing products - stays prepare-but-don't-execute even with that key in hand. Claude drafts the action, a person clicks confirm. This isn't extra caution for its own sake - it's the same boundary Claude already operates under around moving money, and it matches Stripe's own least-privilege guidance for exactly this kind of access.
Timing: not urgent - nothing to report on with zero paying clients. Worth doing once there's real subscription activity to actually watch.
api/chat.js's system prompt is cache_control-marked and confirmed
working (see "What's built" below), but testing found Haiku's minimum
cacheable prompt size sits above what a typical business's config
actually reaches even with the new 1000-char training text - so this is
correctly wired for the businesses with the largest configs, not a
general win yet. Real levers still on the table, logged here for later
once there's volume to justify the complexity:
.fd-msg.fd-bot never set its own text color, so it silently
inherited whatever color the host page uses - color is an inherited
CSS property and crosses the Shadow DOM boundary unless explicitly reset,
unlike the non-inherited properties Shadow DOM blocks automatically. On
a dark host page with light body text (exactly what our own site is),
that leaked straight into the widget's white message bubbles, producing
near-invisible light-on-white text - confirmed from a real screenshot,
then reproduced and fixed: added color: #1a1a1a to .fd-root as an
explicit baseline. Verified against the exact reported case (computed
color was rgb(26, 26, 26) on white afterward, confirmed visually too).#ff7a59) only has a 2.6:1 contrast ratio
with white - fails WCAG AA, and any pale custom color (tested with
#f5e6a8) made text nearly invisible. Fixed with contrastTextColor()
#1a1a1a..plan-cta buttons to both plans, plus
hover/active states (lift + brightness on hover, press-down on click)
across every button on the site, not just the new ones.href to the
real Stripe Payment Link the moment it exists.The closed bubble used to just sit there statically. Now: pops in with a
little spring on page load, a soft breathing glow while idle (color-matched
to each business's own accent color via color-mix()), an irregular blink
on Sia's drawn eyes (only the default face - never animates a business's
own uploaded logo), and a one-time, dismissible teaser bubble ("👋 Got a
question? I'm Sia.") that appears after ~4.5s and auto-hides after 9s if
ignored. All of it stops the instant someone actually opens the panel -
the goal is a first glance, not a nagging loop. Verified the full
lifecycle live: pop-in and pulse active on load, teaser appears on
schedule, and clicking it opens the panel, stops all animation, and
removes the teaser cleanly.
The widget was competent but generic - looked like every other blue SaaS
chat bubble (Intercom, Drift, Tawk.to). Redesigned around an actual
identity instead of a chat icon: every install's assistant is named
"Sia" by default (theme.assistantName to override), with a warm
illustrated face (inline SVG, not a fake stock-photo human - honestly
not-a-real-person, but alive and friendly) rather than a generic
speech-bubble outline. The panel now has a rounded speech-bubble tail
connecting it back to the bubble it came from, softer corners throughout,
and a warm coral default accent (#ff7a59) instead of corporate blue -
though a business's own brand color, once set, always wins. Verified
backward-compatible: the existing dentist config (no assistantName, no
avatarUrl) picked up the new defaults automatically while keeping their
own blue accent color untouched.
api/chat.js's buildSystemPrompt() - the single function
used for every business, preview and committed alike - was purely
reactive: answer if the fact was in the business's own "teach your AI"
text, otherwise punt to a human. Restructured into two explicit blocks: a
CORE behaviour section (answer first, then keep the conversation moving;
ask one relevant follow-up at a time rather than interrogating; build up
intent gradually; once there's a real opportunity, naturally offer the
business's actual conversion action - booking, a call-out, a viewing, an
enquiry - instead of a generic "leave your details"; never ask for
contact info prematurely or more than once) that's identical for every
business and can't be overridden by their own supplied text, and a
BUSINESS-SPECIFIC KNOWLEDGE section (unchanged in spirit) for facts only.
Added a type field (the existing onboarding "what do you do?"
selection, previously only baked into greeting text and then discarded)
through shared/build-config.js and both config sanitizers so the core
behaviour knows which conversion action to name per business. Verified
live against the demo config with the exact "leaking taps" scenario this
was designed around - it answered first, asked one relevant follow-up
about urgency, and only asked for a callback number once the visitor
confirmed they wanted it fixed that day.api/weekly-digest.js). A Vercel Cron job
(vercel.json's crons, Mondays 9am UTC) emails every active business a
summary of leads their receptionist captured that week, so the value
stays visible after the sale instead of going quiet - a real churn risk
otherwise. Scoped deliberately to lead counts, not raw chat-message
volume: leads are rare enough that logging one commit per lead
(api/_lib/leadLog.js, reusing the existing GitHub-Contents-API pattern
under api/_private-configs/leads/{key}.json - never publicly reachable,
same reasoning as notifyEmail) is a reasonable fit, but tracking every
chat message is not; that would need a real counter store (Vercel KV /
Upstash) rather than a commit per event. api/lead.js now logs alongside
(in parallel with, never blocking) the real Resend notification. The cron
endpoint itself is protected by CRON_SECRET, which Vercel automatically
sends as a Bearer token on scheduled invocations.configs/{key}.json is served to the public internet as-is (the widget
fetches it client-side), so notifyEmail had no business living there -
proved it live with a plain curl against a real (test) client's config.
First fix attempt (a rewrite to 404 a
path) silently didn't work - Vercel serves an existing static file at a
matching path before ever consulting rewrites. Fixed properly by moving
private data under instead () -
anything there is never served as a static file at all, a real guarantee
rather than a routing rule a file can shadow. Re-verified live after the
correction.SECURITY.md.site/success.html, not a fake
instant success). Worth revisiting for a real database once signup volume
makes that delay or the "each signup triggers a redeploy" pattern a
genuine problem, not before.configs/avatars/ forever, with no cleanup
path yet. Every photo ever uploaded through the self-serve tool gets
committed and stays, even if that visitor never actually pays - low
volume makes this a non-issue today, but worth a cleanup job (delete
orphaned uploads with no matching config after some window) once there's
real traffic through this tool.configs/ dentistw4.json currently has a placeholder (TODO-add-real-contact- [email protected]) since I don't have their real one from the public
site. Needs a real address before this goes live for them.rush-app and
miser-ai) - api/chat.js and api/lead.js both need Vercel
specifically (or another Node serverless host), not a plain static
host.miser-ai). The original static Payment Link was enough to
start; automated trial signup (see below) now needs real API access
instead.price_... id (don't create a new
product) - into STRIPE_PRICE_ID.https://<your-domain>/api/stripe- webhook, subscribed to checkout.session.completed, customer. subscription.updated, and customer.subscription.deleted - copy its
signing secret into STRIPE_WEBHOOK_SECRET. Do this after the first
deploy with the new code, since the endpoint needs to exist first.GITHUB_TOKEN (plus GITHUB_OWNER/GITHUB_REPO). Fine-grained tokens
expire (max 1 year) - worth a calendar reminder to rotate it.vercel.jsonconfigs/private/api/api/_private-configs/{key}.jsonapi/chat.js and
api/lead.js were hardcoded to Access-Control-Allow-Origin: *, unlike
the signup-flow endpoints which already locked to ALLOWED_ORIGIN - they
couldn't use a single fixed origin since a client's widget calls them
from that client's own website, not from ours. Added a domain field
(new, optional, flows through signup's new "Your website's address"
field → Stripe metadata → committed config) checked per-request against
the incoming Origin header (api/_lib/cors.js) once the businessKey
resolves to a config. A business with no domain set yet only works from
our own site - correct, since that matches anywhere it's actually
installed. Verified live: our own site works, a client's real domain
works, an unrelated site pretending to be that client gets a 403.site/signup.html) replacing the old
"build inline on the homepage, then a second confusing button appears"
flow. Both the homepage builder and the pricing section's trial button
now lead to this one page - homepage hands its draft off via
sessionStorage (read once on load, then cleared, so a later refresh
doesn't clobber edits), the pricing button is now a plain link, no JS.
New on this page: an email field captured early via
api/create-checkout.js's action=capture-email (before Stripe, so an
abandoned signup leaves a trace instead of none - log-only for now, no
automated follow-up yet; originally its own file,
api/capture-lead-email.js, merged in later to free a function slot
for api/whatsapp.js under the Vercel Hobby plan's 12-function cap),
PDF upload for training text (parsed entirely
client-side via pdf.js, dynamic-import()-loaded from cdnjs since 6.x
ships as an ES module with no UMD global build - extracted text drops
into the same 1000-char textarea the manual-typing path already fully
handles, so this needed zero new backend surface), and a live
mock-website preview - a generic site layout that updates as fields
change, with the real widget mounted inside it via sendConfigInline
and kept visually contained in the mock browser frame via a CSS
transform trick (position: fixed descendants otherwise escape to
the real viewport instead of looking "inside" the mockup). Widget
remount is debounced (500ms) so typing doesn't destroy/recreate it
every keystroke. api/create-checkout.js now forwards the captured
email as the Checkout Session's customer_email - verified live that
it actually shows up pre-filled on Stripe's own page. Found and fixed a
real bug while building this: the initial pdf.js version/URL was
guessed rather than checked and 404'd outright - re-verified against
cdnjs's own API instead of guessing twice.api/chat.js marks the system prompt cache_control: {type: "ephemeral"} so it's ready to benefit from Anthropic's ~90%
cache-read discount ($0.10/M tokens vs. $1.00/M fresh, current Haiku 4.5
pricing) - implemented and verified working (a deliberately oversized
test prompt showed a clean cache write on the first call and a cache
read on the second, confirming the mechanism itself is correct). Real
finding from that test, worth knowing: Haiku's minimum cacheable
prompt size sits well above ~2,200 tokens (confirmed via testing - exact
published number is inconsistent across sources), and a typical business's
full system prompt, even maxed out at 1000 characters of training text
plus several FAQs, usually won't reach that floor. So caching is
correctly wired and will kick in for unusually large configs, but isn't
actually saving most businesses money today - the honest takeaway is
that the 1000-char increase itself is a small enough cost bump in
absolute terms (a few hundred extra input tokens per message) that it
didn't need the caching win to justify it. A lightweight log line
(cache usage: ...) in api/chat.js reports real read/created token
counts per request whenever caching does engage, so this stays
verifiable rather than assumed as usage grows. Verified the 1000-char
cap survives the full pipeline end-to-end (free preview, paid signup
draft, both sanitizers) without silently truncating back down to the
old FAQ-answer cap that used to sit at 300.widget/frontdesk-widget.js - the embeddable widget. Shadow-DOM
isolated, mobile-responsive, XSS-safe, single <script> tag integration.
Verified in browser preview at both desktop and mobile widths, and the
FAQ-matching logic tested end-to-end (question in, correct answer out).configs/dentistw4.json - real public info only (services, hours-page
content, location, phone, NHS/private, Invisalign, implants, whitening)
scraped from their live site. No fabricated claims.demo/dentistw4.html - working demo page, ready to deploy and link to
in outreach.SECURITY.md - plain-language data/security overview for whoever
reviews this on the prospect's side.outreach/dentistw4-email-draft.md - draft cold email, not sent.api/chat.js - Vercel serverless function proxying to Claude Haiku,
scoped system prompt, input/history/rate caps, graceful fallback to
keyword matching on any failure. API key is a server-side env var only,
never in client code. Not deployed yet - needs your Anthropic API key
(see "What needs you" below).tools/config-generator.html - fill in a form (business info, theme,
FAQs), get a valid config JSON + the exact install snippet back. This is
what makes personalizing prospect #2, #3, #50 fast instead of hand-
writing JSON each time. Tested end-to-end (form → generated JSON →
validated it parses correctly).window.FrontdeskWidget.mount() API -
not a mockup screenshot, the real thing, personalized live. Runs on the
real Claude backend (sendConfigInline: the visitor's own draft
config travels with each chat request instead of needing a config file
on disk first), sanitized and capped server-side exactly like a real
client's config (see sanitizePreviewConfig() in api/chat.js) - proven
resistant to a direct prompt-injection attempt typed into the business
name field. A free-text box (100 chars, capped both client- and
server-side) lets the visitor teach it one real fact about their
business, which then actually shows up in the AI's answers - verified
live end-to-end (typed a made-up fact, asked a question only that fact
could answer, got it back correctly). Found and fixed a real bug while
testing the original version of this: the preview and the generic
example widget stacked exactly on top of each other at the same screen
position: fixed by swapping out the generic one the first time a real
preview is generated, so there's only ever one bubble on screen.hours field per config (per day,
open/close times). Outside those hours, the greeting and header subtitle
automatically switch to an afterHoursGreeting - verified live (tested
at 11:21pm against 8am-6pm hours, correctly showed the closed-but-still-
helping message). This is the single most demo-able feature for
convincing a skeptical owner: it visibly proves the "works while you're
asleep" pitch instead of just claiming it.api/chat.js). Claude does this natively; needed to be told to,
not built from scratch.theme.position
(left/right), theme.avatarUrl (business logo in the bubble/header),
theme.accentColor. Each business's config controls its own look; the
widget code never needs touching per-client.INSTALL.md - copy-paste install steps for WordPress, Squarespace, Wix,
Shopify, and plain HTML. This is what makes genuine self-serve possible,
and also the natural upsell: businesses that don't want to touch any of
this are exactly who pays extra for the "we'll install it for you" tier.site/index.html - the actual company site, with the widget live on it
using a fictional example business. Tested end-to-end (asked it a real
question, got the right answer). This is the better outreach destination
going forward - answers "who is this and how does it work," not just
"here's a mockup."api/lead.js + the widget's "leave your details" form - real lead
capture. Visitor opts in, fills name + contact, it's emailed straight to
the business's configured address (never stored in a database on our
side) along with recent conversation context. Tested end-to-end: form
submission, missing-field validation, unknown-business handling, and the
"not configured yet" safe fallback all verified. Needs a Resend account
(see below) to actually deliver for a real client - until then it logs
server-side instead of silently losing leads.api/create-checkout.js starts a real Stripe Checkout Session for a
7-day free trial (card collected upfront, so Stripe auto-charges the
standard rate the moment the trial ends - no manual "chase payment"
step), api/stripe-webhook.js verifies the signed checkout.session. completed event and publishes configs/{key}.json by committing it
straight to this repo via the GitHub API (the same auto-deploy pipeline
that already publishes every other config, just done by an API call
instead of by hand), and site/success.html polls until it's live and
hands over the real install snippet. shared/build-config.js is the one
place that turns a draft into a config, shared by the free preview tool
and the webhook so they can never drift into two different shapes.
Cancelling (or a failed renewal) flips the config to active: false via
customer.subscription.updated/.deleted, and both the widget itself
and api/chat.js/api/lead.js refuse to operate for an inactive
business - cancelling actually takes the bot offline, not just stops
billing.api/upload-avatar.js commits the uploaded photo to this repo before
checkout starts (same GitHub-as-storage pattern as configs themselves,
reusing api/_lib/github.js), and only a short filename reference travels
through Stripe. The webhook resolves that reference back into a real URL
when it publishes the config. Validated server-side regardless of what
the browser already checked: image MIME type restricted to PNG/JPEG/
WebP/GIF (SVG deliberately excluded - it can embed scripts), 2MB cap,
and api/_lib/config.js's sanitizeCommittedConfig() only ever accepts
an avatar URL matching our own generated upload path or one of the
legacy presets - never an arbitrary caller-supplied URL.