Skip to content

Repository files navigation

VectorShift Frontend Technical Assessment

This is a small visual pipeline builder — drag nodes onto a canvas, wire them together, and hit submit to have a backend tell you how many nodes/edges you've got and whether your pipeline is actually a valid DAG (no cycles). It started as a bare-bones scaffold and this repo is my completed take on the four-part assessment.

Running it

Frontend

cd frontend
npm i
npm start

Opens at http://localhost:3000.

Backend

cd backend
pip install -r requirements.txt
uvicorn main:app --reload

Runs at http://localhost:8000. Start this before you click Submit on the frontend, or you'll just get an error alert.

There's a myenv virtualenv folder in backend/ that came with the project but doesn't actually have anything installed in it — I added requirements.txt so pip install -r requirements.txt (in myenv or wherever you run uvicorn from) gets you the three packages the backend actually needs: fastapi, uvicorn, and pydantic.


What I actually did

Part 1 — Node abstraction

The four original nodes (Input, Output, LLM, Text) were basically the same ~40 lines copy-pasted four times: a bordered div, a title, some inputs/selects wired to local state, and a couple of Handles. So I pulled all of that into one component, nodes/BaseNode.js, that takes three things:

  • a title,
  • a list of fields (each one just says what kind of input it is — text, select, textarea, number — and what its label/default/options are),
  • a list of handles (id, source or target, which side, and any positioning).

Give it those three things and it renders the whole card. So the original four nodes shrank down to short files that just describe what they contain, not how to render it — no more copy-pasted markup.

To prove the abstraction actually saves time, I added five new nodes using nothing but that same config: Math (two inputs, an operation dropdown, one output), Filter, Delay, API, and Database. None of them do anything real — the assessment explicitly says not to bother with real logic here — but each one took maybe 15 lines to write, which is the whole point.

One thing worth flagging: the old Output node had a dropdown where the value was "File" but the visible label said "Image" — clearly a typo in the original code. My generic select renderer always shows the same text for the value and the label, so that quirk is gone now (it just says "File"). I didn't think it was worth building extra complexity into BaseNode to preserve what was almost certainly a bug, but flagging it in case you disagree.

Also — the store already had an updateNodeField function sitting there unused by any of the original nodes. I wired it into BaseNode so that editing a field now actually persists to the Zustand store, not just to local component state. That's a small behavior change beyond a pure refactor, so wanted to call it out rather than slip it in quietly.

Part 2 — Styling

Nothing had any real styling before — just inline border: 1px solid black everywhere. I added one stylesheet (src/styles.css, plain CSS, no new libraries) and gave the whole thing a consistent look:

  • a dark toolbar with rounded pill-shaped draggable nodes, each with its own accent color and a little colored stripe so you can tell node types apart at a glance,
  • white card-style nodes with a matching colored border, clean spacing, and focus rings on the inputs that match each node's color,
  • the ReactFlow canvas itself got a lighter background, and I restyled the built-in controls, minimap, and connection handles so they don't look like default ReactFlow styling anymore,
  • a proper pill-shaped Submit button instead of a bare <button>.

Nine node types now each get their own accent color: green for Input, indigo for LLM, orange for Output, amber for Text, and cyan/pink/violet/blue/red for the five new ones.

Part 3 — Text node logic

Two things were asked for here, both on the Text node specifically:

  1. Grow with content. As you type more text (or add line breaks), the node should visibly get wider and taller. I based this on the longest line's length and the number of lines, with sensible min/max caps, rather than trying to do pixel-perfect text measurement — it's a heuristic, but it grows and shrinks smoothly and looks right in practice.
  2. {{ variable }} → Handle. Type something like {{ input }} into the text box and a new Handle appears on the left side of the node, one per unique variable name, updating live as you type. I used a regex that only matches valid JS variable names (letters/underscore/$ to start, then alphanumerics), so {{ 123abc }} won't create a handle but {{ my_var }} will.

To make this work cleanly, I gave BaseNode two small optional extras: a style prop (so a node can override its own size) and a children override (so a node can supply fully custom body content instead of the generic field list). Only the Text node uses either of these — everything else still goes through the plain config path from Part 1.

Part 4 — Backend integration

  • Frontend (submit.js) now actually does something: it grabs the current nodes/edges straight from the store and POSTs them as JSON to the backend when you click Submit. When the response comes back, it shows a plain browser alert() telling you the node count, edge count, and whether it's a DAG (as a friendly Yes/No, not a raw boolean). If the backend's not running or something goes wrong, you get an alert about that instead of the app just silently failing.

  • Backend (main.py) — I rewrote the /pipelines/parse endpoint. It used to be a GET stub that didn't do anything. It's now a POST endpoint that accepts the pipeline as a JSON body (I skipped the Form(...) approach the original stub hinted at, since that needs python-multipart, which wasn't installed anywhere on this machine — JSON is the more natural fit for this data anyway and needs nothing extra). It counts the nodes and edges, and checks whether the graph is a DAG using a standard three-color depth-first cycle check (basically: mark a node "in progress" while you're exploring its neighbors, and if you ever walk back into a node that's still "in progress," that's a cycle). Also added CORS so the frontend on port 3000 can actually talk to the backend on port 8000 — without that, the browser blocks the request outright.

I actually tested this end-to-end rather than just trusting the code — ran both servers, drove a real headless Chromium browser through dragging an Input and an Output node onto the canvas, connecting them, and clicking Submit, and confirmed the real alert that popped up said:

Pipeline Summary

Nodes: 2
Edges: 1
Forms a DAG: Yes

Also directly checked the DAG logic against a cyclic graph to make sure it correctly says "No" when it should.


Known trade-offs (my calls, open to being wrong)

  • Output node's Type dropdown shows "File" for both options now instead of the original mismatched "Text"/"Image" — see Part 1 above.
  • Field edits now sync to the Zustand store (previously they didn't, even though the store supported it) — see Part 1 above.
  • Text node sizing is a character/line-count heuristic, not exact text measurement — see Part 3 above.
  • There's one pre-existing lint warning in ui.js (a useCallback missing a couple of dependencies) that was already there before I touched anything — it doesn't break npm start, only a strict CI=true production build, so I left it alone since it's outside what was actually asked for.

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages