├── .envrc ├── .github ├── FUNDING.yml ├── images │ └── skidhw-thumbnail.png └── workflows │ └── docker-image.yml ├── pnpm-workspace.yaml ├── open-next.config.ts ├── postcss.config.mjs ├── next.config.js ├── src ├── lib │ ├── qwen.ts │ ├── utils.ts │ └── chat-seed.ts ├── ai │ ├── chat-types.ts │ ├── request.ts │ ├── prompts │ │ ├── global.ts │ │ └── tools │ │ │ └── math-graphing.ts │ ├── openai.ts │ ├── gemini.ts │ └── response.ts ├── app │ ├── init │ │ └── page.tsx │ ├── chat │ │ └── page.tsx │ ├── page.tsx │ ├── settings │ │ ├── page.tsx │ │ └── import │ │ │ └── page.tsx │ ├── layout.tsx │ └── providers.tsx ├── utils │ ├── shuffle.ts │ ├── encoding.ts │ └── image-post-processing.ts ├── @types │ └── i18next.d.ts ├── hooks │ ├── use-native-camera.ts │ ├── use-shortcut.ts │ ├── use-media-query.ts │ └── useQwenHintAutoToggle.ts ├── components │ ├── ShortcutHint.tsx │ ├── customized │ │ └── slider │ │ │ └── slider-06.tsx │ ├── ui │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── collapsible.tsx │ │ ├── kbd.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── badge.tsx │ │ ├── alert.tsx │ │ ├── text-shimmer.tsx │ │ ├── tooltip.tsx │ │ ├── tabs.tsx │ │ ├── slider.tsx │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── alert-dialog.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ └── command.tsx │ ├── UploadsInfo.tsx │ ├── ProblemList.tsx │ ├── guards │ │ └── RequireAiKey.tsx │ ├── GlobalTraitsEditor.tsx │ ├── Explanation.tsx │ ├── InfoTooltip.tsx │ ├── dialogs │ │ ├── TextInputDialog.tsx │ │ ├── settings │ │ │ ├── ShareAISourceDialog.tsx │ │ │ └── AddAISourceDialog.tsx │ │ ├── InspectDialog.tsx │ │ └── ImproveSolutionDialog.tsx │ ├── chat │ │ ├── chat-composer.tsx │ │ ├── chat-messages.tsx │ │ ├── page.tsx │ │ ├── chat-sidebar.tsx │ │ └── chat-header.tsx │ ├── theme-provider.tsx │ ├── markdown │ │ ├── MarkdownRenderer.tsx │ │ └── diagram │ │ │ └── ForceDiagram.tsx │ ├── settings │ │ ├── ExplanationModeSelector.tsx │ │ └── AIAPICredentialsManager.tsx │ ├── areas │ │ └── ActionsArea.tsx │ ├── StreamingOutputDisplay.tsx │ ├── cards │ │ └── ActionsCard.tsx │ └── ShortcutRecorder.tsx ├── i18n.ts ├── store │ ├── chat-db.ts │ └── settings-store.ts └── index.css ├── wrangler.toml ├── i18next.config.ts ├── .gitignore ├── components.json ├── eslint.config.mjs ├── flake.lock ├── index.html ├── flake.nix ├── public └── skid-homework.svg ├── tsconfig.json ├── Dockerfile ├── package.json ├── README-EN.md └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://996every.day/donate"] 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@swc/core' 3 | - '@tailwindcss/oxide' 4 | - esbuild 5 | -------------------------------------------------------------------------------- /.github/images/skidhw-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubewhy/skid-homework/HEAD/.github/images/skidhw-thumbnail.png -------------------------------------------------------------------------------- /open-next.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCloudflareConfig } from "@opennextjs/cloudflare"; 2 | 3 | export default defineCloudflareConfig(); 4 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: "standalone", 4 | }; 5 | 6 | export default nextConfig; 7 | -------------------------------------------------------------------------------- /src/lib/qwen.ts: -------------------------------------------------------------------------------- 1 | export const QWEN_BASE_URL = "https://api.earthsworth.org/v1"; 2 | export const QWEN_TOKEN_URL = "https://api.earthsworth.org/console/token"; 3 | -------------------------------------------------------------------------------- /src/ai/chat-types.ts: -------------------------------------------------------------------------------- 1 | export type AiChatRole = "system" | "user" | "assistant"; 2 | 3 | export type AiChatMessage = { 4 | role: AiChatRole; 5 | content: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/app/init/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import InitPage from "@/components/pages/InitPage"; 3 | 4 | export default function InitRoute() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | # main = ".open-next/worker.js" 2 | name = "skid-homework" 3 | compatibility_date = "2025-12-13" 4 | compatibility_flags = ["nodejs_compat"] 5 | send_metrics = false 6 | 7 | [assets] 8 | directory = ".vercel/output/static" 9 | binding = "ASSETS" 10 | -------------------------------------------------------------------------------- /i18next.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "i18next-cli"; 2 | 3 | export default defineConfig({ 4 | locales: ["en", "zh"], 5 | extract: { 6 | input: "src/**/*.{js,jsx,ts,tsx}", 7 | output: "public/locales/{{language}}/{{namespace}}.json", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/shuffle.ts: -------------------------------------------------------------------------------- 1 | export function shuffleArray(input: T[]): T[] { 2 | const arr = [...input]; 3 | for (let i = arr.length - 1; i > 0; i--) { 4 | const j = Math.floor(Math.random() * (i + 1)); 5 | [arr[i], arr[j]] = [arr[j], arr[i]]; 6 | } 7 | return arr; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatPage from "@/components/chat/page"; 2 | import RequireAiKey from "@/components/guards/RequireAiKey"; 3 | 4 | export default function ChatRoute() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import RequireAiKey from "@/components/guards/RequireAiKey"; 2 | import ScanPage from "@/components/pages/ScanPage"; 3 | 4 | export default function HomePage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import SettingsPage from "@/components/settings/SettingsPage"; 2 | import { Suspense } from "react"; 3 | 4 | export default async function SettingsRoute() { 5 | return ( 6 | Loading...}> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/@types/i18next.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated by i18next-cli. Do not edit manually. 2 | import Resources from './resources'; 3 | 4 | declare module 'i18next' { 5 | interface CustomTypeOptions { 6 | enableSelector: false; 7 | defaultNS: 'translation'; 8 | resources: Resources; 9 | } 10 | } -------------------------------------------------------------------------------- /src/hooks/use-native-camera.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | export const useNativeCamera = () => { 3 | // Capacitor camera is not available in the Next.js build; fall back to web file input. 4 | const isNative = useMemo(() => false, []); 5 | 6 | const capture = useCallback(async (): Promise => [], []); 7 | 8 | return { isNative, capture }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/ShortcutHint.tsx: -------------------------------------------------------------------------------- 1 | import { Kbd } from "./ui/kbd"; 2 | import { formatShortcutLabel } from "@/utils/shortcuts"; 3 | 4 | export interface ShortcutHintProps { 5 | shortcut?: string | null; 6 | } 7 | 8 | export function ShortcutHint({ shortcut }: ShortcutHintProps) { 9 | const label = formatShortcutLabel(shortcut); 10 | if (!shortcut || !label) return null; 11 | return {label}; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nix 2 | /.direnv 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | pnpm-debug.log* 11 | lerna-debug.log* 12 | 13 | node_modules 14 | dist 15 | /.open-next 16 | /.wrangler 17 | /.next 18 | dist-ssr 19 | *.local 20 | /next-env.d.ts 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | .idea 26 | .DS_Store 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | -------------------------------------------------------------------------------- /src/app/settings/import/page.tsx: -------------------------------------------------------------------------------- 1 | import ImportSettingsPage from "@/components/pages/ImportSettingsPage"; 2 | import type { Metadata } from "next"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Getting start - Skid Homework", 6 | description: 7 | "Getting start with the most powerful open source AI homework solver. Time-saving, no telemetry, free.", 8 | }; 9 | export default function ImportSettings() { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/encoding.ts: -------------------------------------------------------------------------------- 1 | export function uint8ToBase64(uint8Array: Uint8Array) { 2 | let binary = ""; 3 | const len = uint8Array.byteLength; 4 | for (let i = 0; i < len; i++) { 5 | binary += String.fromCharCode(uint8Array[i]); 6 | } 7 | return window.btoa(binary); 8 | } 9 | 10 | export async function fileToBase64(file: File) { 11 | const base64 = uint8ToBase64(new Uint8Array(await file.arrayBuffer())); 12 | const base64Url = `data:${file.type};base64, ${base64}`; 13 | 14 | return base64Url; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/customized/slider/slider-06.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Slider } from "@/components/ui/slider"; 4 | import { useState } from "react"; 5 | 6 | export default function SliderWithLabelDemo() { 7 | const [progress, setProgress] = useState([30]); 8 | 9 | return ( 10 |
11 | 12 | {progress[0]}% 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | import nextTs from "eslint-config-next/typescript"; 4 | 5 | const eslintConfig = defineConfig([ 6 | ...nextVitals, 7 | ...nextTs, 8 | // Override default ignores of eslint-config-next. 9 | globalIgnores([ 10 | // Default ignores of eslint-config-next: 11 | ".next/**", 12 | "out/**", 13 | "build/**", 14 | "dist/**", 15 | "next-env.d.ts", 16 | ]), 17 | ]); 18 | 19 | export default eslintConfig; 20 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../index.css"; 2 | import type { Metadata } from "next"; 3 | import Providers from "./providers"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Skid Homework", 7 | description: 8 | "The open source workaround for self-learners. Time-saving, no telemetry, free.", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { Toaster as Sonner, type ToasterProps } from "sonner"; 3 | 4 | const Toaster = ({ ...props }: ToasterProps) => { 5 | const { theme = "system" } = useTheme(); 6 | 7 | return ( 8 | 20 | ); 21 | }; 22 | 23 | export { Toaster }; 24 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1764950072, 6 | "narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "f61125a668a320878494449750330ca58b78c557", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import enCommons from "../public/locales/en/commons.json"; 4 | import zhCommons from "../public/locales/zh/commons.json"; 5 | 6 | const resources = { 7 | en: { commons: enCommons }, 8 | zh: { commons: zhCommons }, 9 | } as const; 10 | 11 | if (!i18n.isInitialized) { 12 | i18n.use(initReactI18next).init({ 13 | resources, 14 | supportedLngs: ["en", "zh"], 15 | lng: "en", 16 | fallbackLng: "en", 17 | ns: ["commons"], 18 | defaultNS: "commons", 19 | debug: false, 20 | interpolation: { 21 | escapeValue: false, 22 | }, 23 | react: { 24 | useSuspense: false, 25 | }, 26 | }); 27 | } 28 | 29 | export default i18n; 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | SkidHomework - AI Powered Homework Solver 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |