CleanFrame
삼성 The Frame용 개인 홈 화면, 포크와 맞춤을 위한 시작점
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Knitting & crochet toolkit for Hong Kong · 織冷工具箱 (plain HTML/CSS/JS on Vite).
From the project folder:
cd "/Users/gracelee/Documents/GitHub/knitflix"
npm install
npm run dev
Open:
http://localhost:5173/app/ automaticallyLegacy redirect:
http://localhost:5173/knitflix.html → /app/Site: knitflix.gracelee.me (Vercel, deploys from main).
index.html: the landing page at / (for now a redirect to /app/)app/index.html: the toolkit at /app/ — six tools, one per URL hash: #home #calc #rows #shops #wiki #refknitflix.html: redirect for the legacy URLpublic/: served as-is at the domain root (no bundling — the scripts are classic, not modules)
assets/css/tokens.css: colours, type, spacing (shared by app and landing)assets/css/app.css: app styles (components)assets/js/app.js: core app logic (navigation + hash routing, lang toggle, counter, calc, toast)assets/js/data/fibers.js, reference.js, shops.js: datasetsassets/js/features/shops.js, wiki.js, reference.js: rendering per toolassets/js/features/pattern.js: AI pattern explainer (Ref → 解讀)assets/data/shops.json: local fallback shop datasetsite.webmanifest: PWA manifest (start_url is /app/)vite.config.js: three HTML entries (/, /app/, knitflix.html), base: '/'Open http://localhost:5173/app/ in dev.
This app can load shop data from a Google Sheet via a Google Apps Script web app that returns JSON.
window.SHOPS_ENDPOINT in index.html to your Apps Script URLassets/data/shops.json if the endpoint is blank/unreachableCreate a tab named shops with a header row containing these columns:
nameZh, nameEn, area, areaZh, areaEn, addr, hrsZh, hrsEn, tel, rating, web, ig, tagsZh, tagsEn, descZh, descEn, online, status
For tagsZh / tagsEn, use | separated text like 實體店|教班|老字號.
Create a new Apps Script project, then deploy as a Web app (Execute as: Me; Who has access: Anyone).
const SHEET_NAME = 'shops';
function doGet() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
return jsonOut({ error: `Missing sheet tab: ${SHEET_NAME}` }, 404);
}
const values = sheet.getDataRange().getValues();
if (values.length < 2) return jsonOut([], 200);
const headers = values[0].map(h => String(h || '').trim());
const rows = values.slice(1);
const data = rows
.filter(r => r.some(cell => String(cell || '').trim() !== ''))
.map(r => rowToShop(headers, r));
return jsonOut(data, 200);
}
function rowToShop(headers, row) {
const o = {};
headers.forEach((h, i) => (o[h] = row[i]));
const toStr = (v) => (v === null || v === undefined ? '' : String(v)).trim();
const toNullStr = (v) => {
const s = toStr(v);
return s ? s : null;
};
const toBool = (v) => {
const s = toStr(v).toLowerCase();
return s === 'true' || s === 'yes' || s === 'y' || s === '1';
};
const toNumOrNull = (v) => {
const s = toStr(v);
if (!s) return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
};
const toTags = (v) => {
const s = toStr(v);
return s ? s.split('|').map(t => t.trim()).filter(Boolean) : [];
};
return {
nameZh: toStr(o.nameZh),
nameEn: toStr(o.nameEn),
area: toStr(o.area),
areaZh: toStr(o.areaZh),
areaEn: toStr(o.areaEn),
addr: toStr(o.addr),
hrsZh: toStr(o.hrsZh),
hrsEn: toStr(o.hrsEn),
tel: toNullStr(o.tel),
rating: toNumOrNull(o.rating),
web: toNullStr(o.web),
ig: toNullStr(o.ig),
tagsZh: toTags(o.tagsZh),
tagsEn: toTags(o.tagsEn),
descZh: toStr(o.descZh),
descEn: toStr(o.descEn),
online: toBool(o.online),
status: toStr(o.status) || 'verify'
};
}
function jsonOut(obj, status) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
main and Folder: / (root)Then the page will be available at:
.../ (recommended).../knitflix.htmlPaste an English knitting or crochet pattern; an AI model explains each abbreviation and translates the pattern line by line (Traditional Chinese, or English when the app is in EN). The answer streams in as it's written, can be stopped part-way, and keeps whatever arrived if the connection drops.
public/assets/js/features/pattern.jsapi/explain-pattern.js, a Vercel Function that calls an OpenAI-compatible chat API with fetch and streams plain text back. No SDK, no extra dependencies.Defaults to Groq's free tier (no credit card) with qwen/qwen3.8-27b.
LLM_API_KEY with that key.Free-tier limits apply (see https://console.groq.com/docs/rate-limits). When they're hit, the app shows a "try again later" message.
To use another OpenAI-compatible provider, also set LLM_BASE_URL and LLM_MODEL (and LLM_REASONING_EFFORT if the model needs it). ALLOWED_ORIGINS adds extra sites allowed to call the endpoint, comma-separated.
npm run dev (Vite) doesn't run the /api function. Use the Vercel CLI instead:
cp .env.example .env.local # then paste your key into .env.local
npx vercel dev