diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 9b4afc8..0000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Docker Frontend Image CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag editor:$(date +%s) diff --git a/.github/workflows/sync-with-pacifiquem.yml b/.github/workflows/sync-with-pacifiquem.yml new file mode 100644 index 0000000..9979de8 --- /dev/null +++ b/.github/workflows/sync-with-pacifiquem.yml @@ -0,0 +1,26 @@ +name: Push to personal github account +on: + push: + branches: [main] + +jobs: + pushit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Remove .github, dist, node_modules + run: rsync * ./built -r --exclude .github --exclude dist --exclude node_modules + + - name: Remove http.extraheader + run: git config --unset-all http.https://github.com/.extraheader + + - name: push to main branch of personal account + uses: cpina/github-action-push-to-another-repository@v1.5.1 + env: + API_TOKEN_GITHUB: ${{ secrets.CPINO_TOKEN }} + with: + source-directory: "built" + destination-github-username: "pacifiquem" + destination-repository-name: "kin-editor" + user-email: pacifiquemurangwa001@gmail.com + target-branch: main diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 682096c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Use the official Node.js 14 image as the base image -FROM node:18-alpine - -# Set the working directory inside the container -WORKDIR /app - -# Copy package.json and package-lock.json to the working directory -COPY package*.json ./ - -# Install project dependencies -RUN npm install - -# Copy the entire project to the working directory -COPY . . - -# Build the Next.js project -RUN npm run build - -# Expose the port that the Next.js application will run on -EXPOSE 3000 - -# Start the Next.js application -CMD ["npm", "start"] diff --git a/app/(site)/page.tsx b/app/(site)/page.tsx index c6f2191..fc1f708 100644 --- a/app/(site)/page.tsx +++ b/app/(site)/page.tsx @@ -1,71 +1,315 @@ "use client"; -import { useContext, useEffect } from "react"; -import KinEditor from "../components/common/editor"; -import NavBar from "../components/layout/navbar"; -import SideBar from "../components/layout/sidebar"; -import { EditorOptions } from "../utils/types"; +import React, { useState, useEffect } from "react"; +import Editor from "@monaco-editor/react"; +import { + Play, + Terminal, + AlignLeft, + BookOpen, + Star, + MessageSquareWarning, + Github, +} from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; -import { KinContext } from "@/components/kin.provider"; -import { Button } from "@/components/ui/button"; -import { Play, Eraser } from "lucide-react"; -const defaultOptions: EditorOptions = { - height: "100%", - width: "100%", - defaultLanguage: "javascript", - theme: "vs-dark", -}; + +interface InputField { + id: string; // generated unique id + variable: string; // inferred variable name or "Input" + prompt: string; // prompt text inside injiza_amakuru("...") + value: string; // current user value +} export default function Home() { - const context = useContext(KinContext); + const [code, setCode] = useState( + `reka x = injiza_amakuru("Andika x: ") +reka y = injiza_amakuru("Andika y: ") + +tangaza_amakuru("Igiteranyo: ", x + y)` + ); + + // Structured inputs derived from code + const [requiredInputs, setRequiredInputs] = useState([]); + + const [output, setOutput] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Parse code to find injiza_amakuru calls + useEffect(() => { + // Regex to match: optional(var = ) injiza_amakuru("prompt") + const regex = + /(?:(reka|ntahinduka)\s+([a-zA-Z_]\w*)\s*=\s*)?injiza_amakuru\s*\(\s*(?:"([^"]*)"|'([^']*)')\s*\)/g; + + let match; + const newInputs: InputField[] = []; + let index = 0; + + while ((match = regex.exec(code)) !== null) { + const varName = match[2] || `Input ${index + 1}`; + const promptText = match[3] || match[4] || "Enter value"; + + const existing = requiredInputs.find((i) => i.id === `input-${index}`); + + newInputs.push({ + id: `input-${index}`, + variable: varName, + prompt: promptText, + value: existing ? existing.value : "", + }); + index++; + } + + if ( + JSON.stringify(newInputs.map((i) => ({ ...i, value: "" }))) !== + JSON.stringify(requiredInputs.map((i) => ({ ...i, value: "" }))) + ) { + setRequiredInputs(newInputs); + } + }, [code]); + + const handleInputChange = (id: string, newValue: string) => { + setRequiredInputs((prev) => + prev.map((field) => + field.id === id ? { ...field, value: newValue } : field + ) + ); + }; - const { editorValue, outputvalue } = context!; + const handleRun = async () => { + const emptyFields = requiredInputs.filter((i) => !i.value.trim()); + if (emptyFields.length > 0) { + setError( + `Please provide values for: ${emptyFields + .map((i) => i.variable) + .join(", ")}` + ); + return; + } + + setIsLoading(true); + setError(null); + setOutput([]); + + const inputString = requiredInputs.map((i) => i.value).join("\n"); + + try { + const res = await fetch("/api/run", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code, input: inputString }), + }); + + const data = await res.json(); + + if (res.ok) { + const safeOutput = Array.isArray(data.output) ? data.output : []; + setOutput(safeOutput); + if (data.error) setError(data.error); + } else { + setError(data.error || "Failed to run code"); + } + } catch (err) { + setError("Network error or server failed to respond."); + } finally { + setIsLoading(false); + } + }; return ( -
- - - - - - - - - - - - - -
- - -
-
- - {outputvalue === "" ? "Output will appear here" : outputvalue} -
-
-
-
-
-
-
+
+ {/* Header */} +
+
+
+ Kin Logo +
+

+ Editor +

+
+ +
+ + +
+ + +
+
+ + {/* Main Content */} +
+ + {/* Editor Panel */} + +
+ setCode(value || "")} + options={{ + minimap: { enabled: false }, + fontSize: 14, + lineNumbers: "on", + scrollBeyondLastLine: false, + automaticLayout: true, + fontFamily: + "JetBrains Mono, Menlo, Monaco, Courier New, monospace", + padding: { top: 24, bottom: 24 }, + renderLineHighlight: "none", + contextmenu: false, + }} + /> +
+
+ + + + {/* Sidebar Panel */} + + + {/* Output Section */} + +
+
+ + + Output + +
+
+ {output.length === 0 && !error && ( + + // Output will ensure here... + + )} + {output.map((line, i) => ( +
+ + $ + + {line} +
+ ))} + {error && ( +
+ + Error: + {" "} + {error} +
+ )} +
+
+
+ + + + {/* Structured Input Form */} + +
+
+ + + Inputs + +
+
+ {requiredInputs.length === 0 ? ( +
+ No inputs detected. +
+ Use{" "} + + injiza_amakuru() + {" "} + to request input. +
+ ) : ( +
+ {requiredInputs.map((field) => ( +
+ + + handleInputChange(field.id, e.target.value) + } + className="w-full bg-neutral-900 border border-white/10 rounded-md px-3 py-2.5 text-sm text-white focus:outline-none focus:border-white/30 focus:bg-neutral-800 transition-all placeholder:text-zinc-700 font-mono" + placeholder="..." + autoComplete="off" + /> +
+ ))} +
+ )} +
+
+
+
+
+
+
); } diff --git a/app/api/run/route.ts b/app/api/run/route.ts new file mode 100644 index 0000000..8e6f0c3 --- /dev/null +++ b/app/api/run/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Parser, Interpreter } from "@kin-lang/kin"; +import { createWebEnv } from "@/lib/runtime"; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { code, input } = body; + + if (!code) { + return NextResponse.json({ error: "No code provided" }, { status: 400 }); + } + + const inputQueue = input ? input.split("\n") : []; + const outputList: string[] = []; + + const env = createWebEnv(inputQueue, outputList); + const parser = new Parser(); + + try { + const program = parser.produceAST(code); + const result = Interpreter.evaluate(program, env); + + let resultString = ""; + if (result.type !== "null") { + resultString = (result as any).value?.toString() ?? result.type; + } + + return NextResponse.json({ + output: outputList, + result: resultString, + }); + } catch (runtimeError: any) { + return NextResponse.json( + { + output: outputList, + error: runtimeError.message || "Unknown Runtime Error", + }, + { status: 500 } + ); + } + } catch (err: any) { + return NextResponse.json( + { error: "Internal Server Error: " + err.message }, + { status: 500 } + ); + } +} diff --git a/app/globals.css b/app/globals.css index 6a75725..0d054f8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,7 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; - + @layer base { :root { --background: 0 0% 100%; @@ -9,63 +9,63 @@ --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; - + --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - + --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; - + --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; - + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - + --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - + --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - + --radius: 0.5rem; } - + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; - + --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; - + --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - + --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; - + --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; - + --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - + --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; - + --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - + --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } - + @layer base { * { @apply border-border; @@ -73,4 +73,20 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} + +@layer utilities { + .custom-scrollbar::-webkit-scrollbar { + width: 6px; + height: 6px; + } + .custom-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + .custom-scrollbar::-webkit-scrollbar-thumb { + @apply bg-white/10 rounded-full hover:bg-white/20 transition-colors; + } + .custom-scrollbar::-webkit-scrollbar-corner { + background: transparent; + } +} diff --git a/lib/runtime.ts b/lib/runtime.ts new file mode 100644 index 0000000..b37657a --- /dev/null +++ b/lib/runtime.ts @@ -0,0 +1,236 @@ +import { + Environment, + MK_NULL, + MK_STRING, + MK_NUMBER, + MK_BOOL, + MK_OBJECT, + MK_NATIVE_FN, + RuntimeVal, + StringVal, + NumberVal, + BooleanVal, + ObjectVal, + FunctionValue, +} from "@kin-lang/kin"; + + +export function matchType(arg: RuntimeVal): any { + switch (arg.type) { + case "string": + return (arg as StringVal).value; + case "number": + return (arg as NumberVal).value; + case "boolean": + return (arg as BooleanVal).value ? "nibyo" : "sibyo"; + case "null": + return "ubusa"; + case "object": + const obj: { [key: string]: unknown } = {}; + const aObj = arg as ObjectVal; + aObj.properties.forEach((value, key) => { + obj[key] = matchType(value); + }); + return obj; + case "fn": + const fn = arg as FunctionValue; + return { + name: fn.name, + body: fn.body, + internal: false, + }; + default: + return arg; + } +} + +export function createWebEnv( + inputQueue: string[], + outputList: string[] +): Environment { + const env = new Environment(); + + env.declareVar("nibyo", MK_BOOL(true), true); + env.declareVar("sibyo", MK_BOOL(false), true); + env.declareVar("ubusa", MK_NULL(), true); + env.declareVar("ikosa", MK_NULL(), false); + + env.declareVar( + "tangaza_amakuru", + MK_NATIVE_FN((args) => { + const output = args + .map((arg) => { + const val = matchType(arg); + return typeof val === "object" ? JSON.stringify(val) : String(val); + }) + .join(" "); + outputList.push(output); + return MK_NULL(); + }), + true + ); + + env.declareVar( + "injiza_amakuru", + MK_NATIVE_FN((args) => { + const nextInput = inputQueue.shift(); + if (nextInput !== undefined) { + // Basic number detection + const numberRegex = /^-?\d+(\.\d*)?$/; + if (numberRegex.test(nextInput)) return MK_NUMBER(Number(nextInput)); + return MK_STRING(nextInput); + } + return MK_NULL(); + }), + true + ); + + const securityError = () => { + throw new Error( + "Security Error: This operation is disabled in the online editor." + ); + }; + env.declareVar("sisitemu", MK_NATIVE_FN(securityError), true); + env.declareVar("hagarara", MK_NATIVE_FN(securityError), true); + env.declareVar("KIN_INYANDIKO", MK_OBJECT(new Map()), true); + + env.declareVar( + "KIN_IMIBARE", + MK_OBJECT( + new Map() + .set("pi", Math.PI) + .set( + "umuzikare", + MK_NATIVE_FN((args) => { + const arg = (args[0] as NumberVal).value; + return MK_NUMBER(Math.sqrt(arg)); + }) + ) + .set( + "umubare_utazwi", + MK_NATIVE_FN((args) => { + const min = Math.ceil((args[0] as NumberVal).value); + const max = Math.floor((args[1] as NumberVal).value); + return MK_NUMBER(Math.floor(Math.random() * (max - min + 1)) + min); + }) + ) + .set( + "kuraho_ibice", + MK_NATIVE_FN((args) => + MK_NUMBER(Math.round((args[0] as NumberVal).value)) + ) + ) + .set( + "sin", + MK_NATIVE_FN((args) => + MK_NUMBER(Math.sin((args[0] as NumberVal).value)) + ) + ) + .set( + "cos", + MK_NATIVE_FN((args) => + MK_NUMBER(Math.cos((args[0] as NumberVal).value)) + ) + ) + .set( + "tan", + MK_NATIVE_FN((args) => + MK_NUMBER(Math.tan((args[0] as NumberVal).value)) + ) + ) + ), + true + ); + + env.declareVar( + "KIN_AMAGAMBO", + MK_OBJECT( + new Map() + .set( + "huza", + MK_NATIVE_FN((args) => { + let res = ""; + args.forEach((arg) => (res += (arg as StringVal).value)); + return MK_STRING(res); + }) + ) + .set( + "ingano", + MK_NATIVE_FN((args) => MK_NUMBER((args[0] as StringVal).value.length)) + ) + .set( + "inyuguti", + MK_NATIVE_FN((args) => + MK_STRING( + (args[0] as StringVal).value.charAt((args[1] as NumberVal).value) + ) + ) + ) + .set( + "inyuguti_nkuru", + MK_NATIVE_FN((args) => + MK_STRING((args[0] as StringVal).value.toUpperCase()) + ) + ) + .set( + "inyuguti_ntoya", + MK_NATIVE_FN((args) => + MK_STRING((args[0] as StringVal).value.toLowerCase()) + ) + ) + .set( + "tandukanya", + MK_NATIVE_FN((args) => { + const str = (args[0] as StringVal).value; + const sep = (args[1] as StringVal).value; + const arr = new Map(); + str + .split(sep) + .forEach((s, i) => arr.set(i.toString(), MK_STRING(s))); + return MK_OBJECT(arr); + }) + ) + ), + true + ); + + env.declareVar( + "KIN_URUTONDE", + MK_OBJECT( + new Map() + .set( + "ingano", + MK_NATIVE_FN((args) => + MK_NUMBER((args[0] as ObjectVal).properties.size) + ) + ) + .set( + "ongera_kumusozo", + MK_NATIVE_FN((args) => { + const obj = args[0] as ObjectVal; + const val = args[1]; + obj.properties.set(obj.properties.size.toString(), val); + return MK_NUMBER(obj.properties.size); + }) + ) + .set( + "ifite", + MK_NATIVE_FN((args) => { + const obj = args[0] as ObjectVal; + const target = (args[1] as StringVal).value; + let found = false; + for (const val of obj.properties.values()) { + if ((val as any).value === target) { + found = true; + break; + } + } + return MK_BOOL(found); + }) + ) + ), + true + ); + + return env; +} diff --git a/package-lock.json b/package-lock.json index 6b7803b..3f299cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "editor", "version": "0.1.0", "dependencies": { + "@kin-lang/kin": "^0.4.2", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-select": "^2.0.0", @@ -177,6 +178,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kin-lang/kin": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@kin-lang/kin/-/kin-0.4.2.tgz", + "integrity": "sha512-13MVzkywWPlZ/fzee0EECl9FICZM8hO9+rIPfUfKa3JV+PWQF3w2IGmbVtY2Mx7qp9jOxQPYgRJoAB4pelKSRw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^14.0.0", + "moment": "^2.30.1", + "prompt-sync": "^4.2.0" + }, + "bin": { + "kin": "dist/bin/kin.js" + } + }, + "node_modules/@kin-lang/kin/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@monaco-editor/loader": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", @@ -1267,6 +1292,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1891,6 +1928,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", @@ -2284,6 +2330,36 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prompt-sync": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", + "integrity": "sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw==", + "license": "MIT", + "dependencies": { + "strip-ansi": "^5.0.0" + } + }, + "node_modules/prompt-sync/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompt-sync/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index b8fcfee..d5a75d4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "docker:run-dev": "docker run -p 3000:3000 -v $(pwd):/app editor" }, "dependencies": { + "@kin-lang/kin": "^0.4.2", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-select": "^2.0.0", diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..82baba3 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file