# Project export: SeniorSidekick

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Verified, one-step-at-a-time tech help for seniors — and when the fix isn't known, a safe handoff to someone they trust instead of a guess
- Devpost: https://devpost.com/software/seniorsidekick-9uymv5
- GitHub: https://github.com/SteveEleven/senior-sidekick
- Demo: https://senior-sidekick.vercel.app/
- Video: https://www.youtube.com/embed/Y-piWvu8gPg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Steve Redden (3 commits)

## Devpost submission (written by the team)

### Inspiration

I've been working on this since I took an AI course where at least 80% of the students were over 60. At home, we're dealing with three seniors in independent living situations at three different locations, with monthly issues — phones, TVs, intercoms. We visit my 99-year-old mother, my 96-year-old father, and my 87-year-old stepfather, all living independently. We live the real-life version of "my TV volume doesn't work." To save us an hour's drive to resolve simple issues, I built SeniorSidekick: an app that starts with the most common problems in a simple way, and documents every step along the way — so if it has to be handed off, back to us or to someone else, several steps have already been completed.

### What it does

SeniorSidekick is a voice-and-text troubleshooting assistant that helps seniors solve everyday technology problems safely and confidently. Instead of generating repair instructions with AI, it guides users through human-verified troubleshooting decision trees using a simple one-question-at-a-time interface. If an issue can't be resolved, the app automatically prepares a concise handoff summary for a trusted family member or helper — including what's already been tried and a scam-safety reminder — so someone else can continue helping without repeating the entire process. The current implementation includes a fully verified end-to-end "TV has no sound" workflow, validated on a real television. How it's different SeniorSidekick is not a chatbot that guesses at fixes, and it is not a remote-control tool. Unlike paid human tech support (scheduled, costs money) or remote-access tools like TeamViewer and AnyDesk — which can be dangerous when a senior is tricked into granting access to a scammer — it gives immediate, human-verified steps for a common problem, then hands off safely to someone the senior already trusts. How I built it Built with Codex inside Visual Studio Code, with the initial scaffold generated in a documented Codex session (session ID in the repository README). I directed the build as the sole author of all verified troubleshooting content, using Codex to implement the GPT-5.6 seams and their deterministic offline fallbacks. Every repair step in the decision tree was written and verified by me — none of it is generated by a model. The architecture intentionally separates deterministic troubleshooting logic from AI-assisted language tasks. Human-verified decision trees define every troubleshooting step, so repair guidance stays predictable, reviewable, and safe. GPT-5.6 is integrated at only two carefully controlled points: intent routing, to identify which verified flow best matches the user's request; and handoff polishing, to rewrite the family-helper summary into clear, natural language. Both AI integrations sit behind a feature flag and include deterministic offline fallbacks, so the app keeps working even when AI services are unavailable. This architecture guarantees AI never generates troubleshooting instructions. Open source under the MIT License. Challenges I ran into The biggest design challenge was deciding where AI should not be used. It would have been easy to let an LLM generate troubleshooting advice dynamically, but that reduces consistency and makes validation difficult. Instead, I restricted AI to tasks where language flexibility adds value, while keeping all troubleshooting logic deterministic and human-verified. Designing an interface that shows only one question at a time also meant balancing simplicity against giving users enough context. Accomplishments that I'm proud of A working end-to-end product on day one: live on Vercel, public repo, clean Codex-attributed commit history from the very first commit. The app never guesses. Getting an AI-era product to say "I don't know — let's ask someone you trust," and making that feel like a win, took real design work. It's the feature I'd defend hardest. Offline-first demo mode: the core flow runs with zero network, because the moment someone needs this app is often the moment their wifi is the problem. The complete TV-no-sound workflow, validated on real hardware. A one-question-at-a-time interface designed to reduce cognitive overload. Automatic family-helper handoff summaries with a built-in scam-safety reminder. What I learned I learned that a 64-year-old dropout who was computer illiterate can, with OpenAI and the other AI's, become capable of operating just about any device — iPhone, drone, software. It came down to constructive conversation with the AI, testing it against other windows to verify they weren't hallucinating, and making regular handoffs so they stay fresh. (That's getting to be less of an issue as they get smarter. Or maybe I am.) Potential impact Older adults face a real independence-and-safety gap with everyday technology. In 2024, adults age 60 and over reported $2.4 billion in aggregate fraud losses to the FTC, including $159 million in losses to tech-support scams. Because fraud is underreported, the FTC estimates the overall cost of fraud to older adults in 2024 was between $10.1 billion and $81.5 billion, depending on methodology. Rather than encouraging open-ended AI advice or unknown remote access, SeniorSidekick offers bounded, human-verified troubleshooting and a safe handoff to a trusted family member. Source: FTC, Protecting Older Consumers 2024–2025 (December 2025), https://www.ftc.gov/system/files/ftc_gov/pdf/P144400-OlderAdultsReportDec2025.pdf Design Large resizable text, high contrast, plain language, persistent navigation, and one decision per screen — grounded in WCAG accessibility principles and W3C findings on older users' needs.

### What's next

Planning Mode — a "family planning preview" that helps adult children set up a senior's device profile, verified trees for their actual devices, and helper contacts before problems happen. Nearer term: more verified trees, voice input with push-to-talk, and testing with the three people who inspired it.

## README (from the GitHub repository)

# SeniorSidekick

An offline-first, large-type troubleshooting assistant for seniors — one clear step at a time, with an optional handoff summary for a trusted helper.

**Live demo:** [https://senior-sidekick.vercel.app](https://senior-sidekick.vercel.app)

**Category:** Apps for Your Life (OpenAI Build Week)

## Try the demo (no setup)

1. Open the [live demo](https://senior-sidekick.vercel.app).
2. Enter: `My TV has no sound` → **Start helping me**.
3. Walk the steps: volume → mute → cable box → success or trusted-helper handoff.
4. Optional: try a different problem (routes to the “other problem” path) or **Start over**.

## How we built this (Codex + GPT-5.6)

Built for [OpenAI Build Week](https://openai.devpost.com/) with **Codex** as the primary coding agent and **GPT-5.6** as the model in that workflow.

### Codex Session ID (primary build thread)

```
019f611d-ad83-79b3-be24-445afe387151
```

Session: **“Scaffold SeniorSidekick”** (July 13, 2026) — the thread where most core functionality was built.

### Collaboration story

| Who | Role |
| --- | --- |
| **Human** | Problem framing (help for seniors when something breaks), product constraints (large type, few choices, offline-capable demo, scam-safety language), decision-tree content for “TV has no sound,” and ship decisions (MIT license, Next.js pin, public GitHub + Vercel). |
| **Codex** | Scaffolded the Next.js + Tailwind app, decision-tree model in `lib/decision-trees.ts`, UI flow in `components/senior-sidekick.tsx` (welcome → steps → success / handoff / other-problem), and iterative features (start-over, scam-safety line on handoff). |
| **GPT-5.6** | Powered the Codex build sessions. The product also reserves explicit **GPT-5.6 seams** in `lib/demo-services.ts` for (1) natural-language intent routing and (2) trusted-helper handoff summaries — stubbed with deterministic offline logic so the demo works without API keys after load. |

Key human decisions Codex executed against:

- Prefer a **guided decision tree** over a free-form chat for accessibility and predictability.
- Ship an **offline-first demo**; keep live-model call sites clearly marked for a later online mode.
- Always give an escape hatch: **handoff to someone I trust**, with copy-shareable summary and a reminder that real helpers never ask for passwords or payment.

## Local development

```bash
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000). No environment variables required for demo mode.

### Optional local GPT-5.6 test mode

The deployed app stays in deterministic demo mode. To test the two live seams locally only, copy `.env.example` to `.env.local`, set `NEXT_PUBLIC_LIVE_GPT_56=true`, and add a server-side `OPENAI_API_KEY`. The live model may route an intent or polish a handoff summary; all troubleshooting steps remain in the human-verified decision tree. If the flag, key, or API call is unavailable, the app silently uses its existing offline behavior.

## Edit the troubleshooting tree

Change steps in `lib/decision-trees.ts`. Intent routing and handoff summary live in `lib/demo-services.ts` (offline stubs + GPT-5.6 seams).

## Deploy

```bash
npx vercel
npx vercel --prod
```

Or import [SteveEleven/senior-sidekick](https://github.com/SteveEleven/senior-sidekick) in the Vercel dashboard. Default Next.js settings work as-is.

## License

MIT — see [LICENSE](./LICENSE).


## Detected evidence (automated analysis)

Indexed codebase: 11 recognized source files, 23 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (17 of 17)

```
.env.example
.gitignore
app/api/assist/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
components/senior-sidekick.tsx
lib/decision-trees.ts
lib/demo-services.ts
LICENSE
next-env.d.ts
next.config.ts
package.json
postcss.config.mjs
README.md
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @types/node@^22.0.0, @types/react@^19.0.0, @types/react-dom@^19.0.0, autoprefixer@^10.4.20, next@15.5.7, postcss@^8.4.41, react@19.1.1, react-dom@19.1.1, tailwindcss@^3.4.10, typescript@^5.5.4

### Recent commits (newest first)

- Built with Codex: harden live handoff seam — forbid inferred outcomes, server-side safety-reminder check with offline fallback
- Built with Codex: live GPT-5.6 seams (intent routing + handoff summary) behind NEXT_PUBLIC_LIVE_GPT_56 flag, off by default with deterministic offline fallback
- docs: add hackathon session details
- Built with Codex: other-problem path, start-over, scam-safety line
- chore: add MIT license, pin Next.js to 15.5.7
- Built with Codex: offline-first scaffold, decision-tree model, GPT-5.6 seams

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "senior-sidekick",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "15.5.7",
    "react": "19.1.1",
    "react-dom": "19.1.1"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.41",
    "tailwindcss": "^3.4.10",
    "typescript": "^5.5.4"
  }
}

```

### app/page.tsx

```typescript
import { SeniorSidekick } from "@/components/senior-sidekick";

export default function Home() {
  return <SeniorSidekick />;
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "SeniorSidekick",
  description: "Simple, step-by-step tech help.",
};

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### app/api/assist/route.ts

```typescript
import { NextResponse } from "next/server";

export const runtime = "nodejs";

const model = "gpt-5.6";

type RouteRequest = {
  action?: unknown;
  problemDescription?: unknown;
  offlineSummary?: unknown;
};

function liveModeIsReady() {
  return process.env.NEXT_PUBLIC_LIVE_GPT_56 === "true" && Boolean(process.env.OPENAI_API_KEY);
}

function outputText(payload: unknown) {
  if (!payload || typeof payload !== "object") return "";
  const response = payload as { output_text?: unknown; output?: unknown };
  if (typeof response.output_text === "string") return response.output_text;
  if (!Array.isArray(response.output)) return "";

  return response.output
    .flatMap((item) => {
      if (!item || typeof item !== "object") return [];
      const content = (item as { content?: unknown }).content;
      if (!Array.isArray(content)) return [];
      return content.map((part) => (
        part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string"
          ? (part as { text: string }).text
          : ""
      ));
    })
    .join("");
}

async function createResponse(body: Record<string, unknown>) {
  const response = await fetch("https://api.openai.com/v1/responses", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model, reasoning: { effort: "low" }, ...body }),
  });

  if (!response.ok) throw new Error("OpenAI response failed");
  return outputText(await response.json());
}

export async function POST(request: Request) {
  if (!liveModeIsReady()) return new NextResponse(null, { status: 404 });

  try {
    const body = await request.json() as RouteRequest;

    if (body.action === "route-intent" && typeof body.problemDescription === "string") {
      const text = await createResponse({
        instructions: "Classify the user's problem for a verified troubleshooting tree. Return tv-no-sound only when it is clearly about television sound; otherwise return other. Do not provide troubleshooting advice, steps, explanations, or extra fields.",
        input: body.problemDescription,
        text: {
          format: {
            type: "json_schema",
            name: "intent_route",
            strict: true,
            schema: {
              type: "object",
              properties: { treeId: { type: "string", enum: ["tv-no-sound", "other"] } },
              required: ["treeId"],
              additionalProperties: false,
            },
          },
        },
      });
      const parsed = JSON.parse(text) as { treeId?: unknown };
      if (parsed.treeId !== "tv-no-sound" && parsed.treeId !== "other") throw new Error("Invalid intent");
      return NextResponse.json({ treeId: parsed.treeId, confidence: "demo" });
    }

    if (body.action === "compose-handoff" && typeof body.offlineSummary === "string") {
      const summary = await createResponse({
        instructions: "Rewrite the supplied trusted-helper handoff note to be concise, clear, and warm. Preserve its facts and safety reminder. Only restate steps and outcomes that appear in the supplied note; do not add, infer, or state any step result, action, or outcome that is not explicitly present in the input; if a step has no recorded answer, do not describe what happened on it. Do not add, recommend, change, or invent troubleshooting steps. Return only the ready-to-share note. End with this exact final line: \"Real helpers never ask for passwords or payment.\"",
        input: body.offlineSummary,
      });
      if (!summary.trim()) throw new Error("Empty summary");
      if (!summary.includes("passwords or payment")) throw new Error("Missing safety reminder");
      return NextResponse.json({ summary });
    }

    return new NextResponse(null, { status: 400 });
  } catch {
    // The browser intentionally retains its deterministic offline fallback.
    return new NextResponse(null, { status: 503 });
  }
}

```

### next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Keep file tracing scoped to this app when a parent directory has a lockfile.
  outputFileTracingRoot: process.cwd(),
};

export default nextConfig;

```

### next-env.d.ts

```typescript
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
  content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"],
  theme: {
    extend: {
      colors: {
        navy: "#12304A",
        sky: "#EAF6FC",
        calm: "#1677A8",
      },
    },
  },
  plugins: [],
};

export default config;

```

### app/globals.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root { color-scheme: light; }

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: #eaf6fc;
  color: #12304a;
  font-family: Arial, Helvetica, sans-serif;
}

button, textarea { font: inherit; }

button:focus-visible, textarea:focus-visible {
  outline: 4px solid #f6a623;
  outline-offset: 4px;
}

```

### lib/decision-trees.ts

```typescript
/**
 * EDIT THIS FILE to replace the demo troubleshooting content.
 * Each tree is an ordered set of named steps. A `nextOnYes` / `nextOnNo` value
 * may point to another step id, "handoff", or "complete".
 */

export type NextAction = string | "handoff" | "complete";

export type DecisionStep = {
  id: string;
  instruction: string;
  question?: string;
  nextOnYes?: NextAction;
  nextOnNo?: NextAction;
  handoff?: boolean;
};

export type DecisionTree = {
  id: string;
  title: string;
  steps: DecisionStep[];
};

export const decisionTrees: DecisionTree[] = [
  {
    id: "tv-no-sound",
    title: "TV has no sound",
    steps: [
      {
        id: "check-volume",
        instruction: "Pick up the TV remote. Press the volume-up button a few times.",
        question: "Can you hear sound now?",
        nextOnYes: "complete",
        nextOnNo: "check-mute",
      },
      {
        id: "check-mute",
        instruction: "Look for a button with a speaker and a line through it. Press it once.",
        question: "Can you hear sound now?",
        nextOnYes: "complete",
        nextOnNo: "check-cable",
      },
      {
        id: "check-cable",
        instruction: "Please check that the power light is on for any small cable box near your TV.",
        question: "Did that solve the problem?",
        nextOnYes: "complete",
        nextOnNo: "handoff",
        handoff: true,
      },
    ],
  },
];

export function getTreeById(id: string) {
  return decisionTrees.find((tree) => tree.id === id);
}

```

### lib/demo-services.ts

```typescript
import { decisionTrees } from "./decision-trees";

export type IntentResult = { treeId: string | "other"; confidence: "demo" };

const liveModeEnabled = process.env.NEXT_PUBLIC_LIVE_GPT_56 === "true";

// GPT-5.6 SEAM: intent routing — currently stubbed for offline demo
export function routeIntentOffline(problemDescription: string): IntentResult {
  const words = problemDescription.toLowerCase();
  const soundsLikeTvSound = /tv|television|sound|volume|mute|hear/.test(words);
  return {
    treeId: soundsLikeTvSound ? decisionTrees[0].id : "other",
    confidence: "demo",
  };
}

// GPT-5.6 SEAM: this is only called when live mode is explicitly enabled.
// Callers retain the offline result if the request cannot be completed.
export async function routeIntentLive(problemDescription: string): Promise<IntentResult> {
  const response = await fetch("/api/assist", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ action: "route-intent", problemDescription }),
  });

  if (!response.ok) throw new Error("Live intent routing is unavailable");
  return response.json() as Promise<IntentResult>;
}

export type TriedStep = { instruction: string; answer?: "yes" | "no" };

// GPT-5.6 SEAM: handoff summary composition — currently stubbed for offline demo
export function composeHandoffSummaryOffline({
  problemDescription,
  treeTitle,
  triedSteps,
}: {
  problemDescription: string;
  treeTitle: string;
  triedSteps: TriedStep[];
}) {
  const tried = triedSteps.length
    ? triedSteps.map((step, index) => `${index + 1}. ${step.instruction} (${step.answer === "yes" ? "said it worked" : "did not work"})`).join("\n")
    : "No troubleshooting steps were completed.";

  return `SeniorSidekick help summary\n\nProblem: ${problemDescription || treeTitle}\n\nSteps tried:\n${tried}\n\nThey would appreciate your help with the next step.\n\nReminder: only share this with someone you know and trust. Real helpers never ask for passwords or payment.`;
}

// GPT-5.6 SEAM: handoff wording only. It never generates troubleshooting steps.
export async function composeHandoffSummaryLive(offlineSummary: string): Promise<string> {
  const response = await fetch("/api/assist", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ action: "compose-handoff", offlineSummary }),
  });

  if (!response.ok) throw new Error("Live handoff composition is unavailable");
  const body = await response.json() as { summary?: unknown };
  if (typeof body.summary !== "string" || !body.summary.trim()) {
    throw new Error("Live handoff composition returned no summary");
  }
  return body.summary;
}

export function isLiveGpt56Enabled() {
  return liveModeEnabled;
}

```

[1 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]