specific knowledge by malvika.jain on instagram
a chat-based interview tool that asks 10 questions to surface a user's specific knowledge, then uses claude to synthesize a personal analysis.
description
specific knowledge is a local single-page web app. it walks a user through 10 fixed questions drawn from naval ravikant's concept of irreplaceable, untrained knowledge, collects their answers in a chat ui, then sends all responses to the anthropic claude api through a small local express proxy to generate a 3-sentence second-person analysis of the user's unique skill intersection.
steps
download the code
- download the project files: index.html, server.js, package.json, and .env.example
- keep them in the same folder
install dependencies
- open a terminal in the project folder
- run npm install
add your anthropic api key
- copy .env.example to .env
- set API=your_anthropic_api_key — server.js reads process.env.API
run the local server
- run npm start (or node server.js)
- open http://localhost:3000 in your browser
- if port 3000 is already in use, run PORT=3001 npm start and open that port instead
complete the interview
- answer all 10 questions in the chat ui — the status footer tracks progress from question 1 of 10 to complete
review the generated analysis
- after question 10, the app calls /api/chat with all answers and displays a 3-sentence specific knowledge summary
- use 'start over' to reload and run a new session
scripts
01 · package.json
download{
"name": "specific-knowledge",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.21.0"
}
}
02 · .env.example
downloadAPI=your_anthropic_api_key
03 · server.js
downloadimport express from 'express';
import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = Number(process.env.PORT) || 3000;
const API_KEY = process.env.API;
if (!API_KEY) {
console.error('Error: API key not found in .env file. Please add API=your-key-here');
process.exit(1);
}
app.use(express.json());
app.use(express.static(__dirname));
// Proxy endpoint for Claude API
app.post('/api/chat', async (req, res) => {
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify(req.body)
});
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json(data);
}
res.json(data);
} catch (error) {
console.error('API Error:', error);
res.status(500).json({ error: { message: error.message } });
}
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
04 · index.html
download<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Specific Knowledge</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #ffffff;
--text: #000000;
--border: #000000;
--muted: #666666;
--light-bg: #f5f5f5;
/* Grid system - 8px base unit */
--grid: 8px;
--space-1: calc(var(--grid) * 1); /* 8px */
--space-2: calc(var(--grid) * 2); /* 16px */
--space-3: calc(var(--grid) * 3); /* 24px */
--space-4: calc(var(--grid) * 4); /* 32px */
--space-5: calc(var(--grid) * 5); /* 40px */
--space-6: calc(var(--grid) * 6); /* 48px */
--space-8: calc(var(--grid) * 8); /* 64px */
/* Typography scale */
--font-xs: 11px;
--font-sm: 13px;
--font-base: 14px;
--font-md: 16px;
/* Safe areas for notched phones */
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px);
}
html, body {
height: 100%;
height: 100dvh;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
body {
font-family: 'JetBrains Mono', monospace;
font-size: var(--font-sm);
line-height: 1.7;
background: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: var(--space-3);
padding-top: calc(var(--space-3) + var(--safe-top));
padding-bottom: calc(var(--space-3) + var(--safe-bottom));
padding-left: calc(var(--space-3) + var(--safe-left));
padding-right: calc(var(--space-3) + var(--safe-right));
}
.container {
width: 100%;
max-width: 600px;
height: 100%;
display: grid;
grid-template-rows: auto 1fr auto;
gap: var(--space-3);
overflow: hidden;
}
/* Header Section */
.intro {
text-align: center;
}
.intro h1 {
font-size: var(--font-sm);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: var(--space-3);
}
.intro p {
text-align: left;
margin-bottom: var(--space-2);
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.intro p:last-child {
margin-bottom: 0;
}
/* Footer */
.footer {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--border);
}
.status {
font-size: var(--font-xs);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.attribution {
font-size: var(--font-xs);
color: var(--muted);
text-align: right;
}
/* Chat Box */
.chat-box {
border: 2px solid var(--border);
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.messages-wrapper {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-2);
-webkit-overflow-scrolling: touch;
min-height: 0;
}
.messages {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.message {
display: grid;
grid-template-rows: auto auto;
gap: var(--space-1);
}
.message-label {
font-size: var(--font-xs);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.message-content {
padding: var(--space-2);
border: 1px solid var(--border);
text-align: left;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.message.user .message-content {
background: var(--light-bg);
}
.message.system .message-content {
background: var(--bg);
}
.typing-indicator {
display: flex;
gap: var(--space-1);
padding: var(--space-2);
border: 1px solid var(--border);
}
.typing-indicator span {
width: 6px;
height: 6px;
background: var(--muted);
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
30% { opacity: 1; transform: translateY(-4px); }
}
/* Input Area */
.input-wrapper {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--space-2);
padding: var(--space-2);
border-top: 2px solid var(--border);
flex-shrink: 0;
}
.chat-input {
width: 100%;
min-width: 0;
padding: var(--space-2);
border: 1px solid var(--border);
font-family: 'JetBrains Mono', monospace;
font-size: var(--font-sm);
line-height: 1.5;
resize: none;
min-height: var(--space-6);
background: var(--bg);
color: var(--text);
-webkit-appearance: none;
appearance: none;
border-radius: 0;
}
.chat-input:focus {
outline: none;
}
.chat-input::placeholder {
color: var(--muted);
opacity: 1;
}
.send-btn {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--border);
background: var(--text);
color: var(--bg);
font-family: 'JetBrains Mono', monospace;
font-size: var(--font-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
white-space: nowrap;
min-height: 44px; /* Touch-friendly target */
}
.send-btn:hover:not(:disabled) {
opacity: 0.8;
}
.send-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Results styling */
.result-section {
margin-top: var(--space-1);
padding-top: var(--space-2);
border-top: 1px dashed var(--border);
}
.result-section p {
margin-bottom: var(--space-2);
}
.restart-link {
display: inline-block;
margin-top: var(--space-2);
color: var(--muted);
text-decoration: underline;
cursor: pointer;
padding: var(--space-1) 0;
}
.restart-link:hover {
color: var(--text);
}
.hidden {
display: none !important;
}
/* ============================================
TABLET BREAKPOINT (481px - 768px)
============================================ */
@media (max-width: 768px) {
body {
padding: var(--space-2);
padding-top: calc(var(--space-2) + var(--safe-top));
padding-bottom: calc(var(--space-2) + var(--safe-bottom));
}
.container {
gap: var(--space-2);
}
.intro h1 {
margin-bottom: var(--space-2);
}
.intro p {
font-size: var(--font-sm);
line-height: 1.6;
}
}
/* ============================================
MOBILE BREAKPOINT (≤480px)
============================================ */
@media (max-width: 480px) {
:root {
--font-xs: 10px;
--font-sm: 12px;
}
body {
font-size: var(--font-sm);
padding: var(--space-2);
padding-top: calc(var(--space-2) + var(--safe-top));
padding-bottom: calc(var(--space-2) + var(--safe-bottom));
align-items: flex-start;
}
.container {
gap: var(--space-2);
}
.intro {
text-align: left;
}
.intro h1 {
margin-bottom: var(--space-2);
text-align: center;
}
.intro p {
font-size: var(--font-sm);
line-height: 1.5;
margin-bottom: var(--space-1);
}
.chat-box {
min-height: 200px;
}
.messages-wrapper {
padding: var(--space-2);
}
.messages {
gap: var(--space-2);
}
.input-wrapper {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
gap: var(--space-2);
padding: var(--space-2);
}
.chat-input {
font-size: 16px; /* Prevents iOS zoom on focus */
min-height: 48px;
padding: 12px;
}
.chat-input::placeholder {
font-size: 14px;
}
.send-btn {
width: 100%;
font-size: 14px;
padding: var(--space-2);
min-height: 48px;
}
.footer {
grid-template-columns: 1fr;
text-align: center;
gap: var(--space-1);
}
.attribution {
text-align: center;
}
.message-content {
padding: var(--space-2);
}
}
/* ============================================
SMALL MOBILE (≤360px)
============================================ */
@media (max-width: 360px) {
:root {
--font-xs: 9px;
--font-sm: 11px;
}
body {
padding: var(--space-1);
padding-top: calc(var(--space-1) + var(--safe-top));
padding-bottom: calc(var(--space-1) + var(--safe-bottom));
}
.container {
gap: var(--space-1);
}
.intro p {
font-size: var(--font-sm);
line-height: 1.4;
}
.messages-wrapper {
padding: var(--space-1);
}
.message-content {
padding: var(--space-1);
}
.input-wrapper {
padding: var(--space-1);
gap: var(--space-1);
}
.chat-input {
min-height: 44px;
padding: 10px;
font-size: 16px;
}
.send-btn {
min-height: 44px;
}
}
/* ============================================
LANDSCAPE MOBILE
============================================ */
@media (max-height: 500px) and (orientation: landscape) {
.intro p {
display: none;
}
.container {
gap: var(--space-1);
}
.intro h1 {
margin-bottom: var(--space-1);
}
}
</style>
</head>
<body>
<div class="container">
<!-- Intro Section -->
<div class="intro">
<h1>Specific Knowledge</h1>
<p>Specific knowledge is knowledge that cannot be trained for. If society can train you, it can train someone else and replace you. It is found by pursuing your genuine curiosity and passion rather than whatever is hot right now.</p>
<p>I'm going to ask you 10 questions to help uncover your specific knowledge. Answer honestly and in detail—I'll ask follow-ups if I need more clarity.</p>
</div>
<!-- Chat Box with hard border -->
<div class="chat-box">
<div class="messages-wrapper" id="messagesWrapper">
<div class="messages" id="messages"></div>
</div>
<div class="input-wrapper" id="inputWrapper">
<textarea
class="chat-input"
id="chatInput"
placeholder="Type your answer..."
rows="1"
autocomplete="off"
></textarea>
<button class="send-btn" id="sendBtn">Send</button>
</div>
</div>
<!-- Footer -->
<div class="footer">
<span class="status" id="status">Question 1 of 10</span>
<span class="attribution">Inspired by Naval Ravikant</span>
</div>
</div>
<script>
// ============================================
// CONFIG
// ============================================
const QUESTIONS = [
"What can you explain effortlessly that others find confusing?",
"What problems do you solve differently than most people?",
"What combinations of skills do you have that are rare?",
"What have you built that required knowledge you couldn't easily Google?",
"What do experts in your field ask you about?",
"What feels obvious to you but isn't obvious to others?",
"What mistakes have you made that taught you something nobody writes about?",
"What can you see coming that others are missing?",
"What questions do you keep returning to across different projects?",
"What would be hardest to teach someone else?"
];
const API_ENDPOINT = '/api/chat';
const MODEL = 'claude-sonnet-5';
// ============================================
// STATE
// ============================================
let currentQuestion = 0;
let history = [];
let answers = {};
let waiting = false;
// ============================================
// DOM
// ============================================
const $ = id => document.getElementById(id);
const dom = {
status: $('status'),
messages: $('messages'),
messagesWrapper: $('messagesWrapper'),
input: $('chatInput'),
sendBtn: $('sendBtn'),
inputWrapper: $('inputWrapper')
};
// ============================================
// UI HELPERS
// ============================================
function scrollToBottom() {
dom.messagesWrapper.scrollTop = dom.messagesWrapper.scrollHeight;
}
function addMessage(content, type) {
const div = document.createElement('div');
div.className = `message ${type}`;
div.innerHTML = `
<span class="message-label">${type === 'system' ? 'Interviewer' : 'You'}</span>
<div class="message-content">${content}</div>
`;
dom.messages.appendChild(div);
scrollToBottom();
}
function showTyping() {
const div = document.createElement('div');
div.className = 'message system';
div.id = 'typing';
div.innerHTML = `
<span class="message-label">Interviewer</span>
<div class="typing-indicator"><span></span><span></span><span></span></div>
`;
dom.messages.appendChild(div);
scrollToBottom();
}
function hideTyping() {
$('typing')?.remove();
}
function setWaiting(state) {
waiting = state;
dom.sendBtn.disabled = state;
}
// ============================================
// API
// ============================================
async function callAPI(prompt, maxTokens = 300) {
const res = await fetch(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: MODEL,
max_tokens: maxTokens,
messages: [{ role: 'user', content: prompt }]
})
});
const data = await res.json();
if (!res.ok) {
console.error('API Error:', data);
const errMsg = data.error?.message || JSON.stringify(data.error) || 'API request failed';
throw new Error(errMsg);
}
return data.content[0].text;
}
async function getAnalysis() {
const formatted = Object.entries(answers)
.map(([i, a]) => `Q${+i + 1}: ${QUESTIONS[i]}\nA: ${a}`)
.join('\n\n');
const prompt = `You are an expert at identifying "specific knowledge" - the unique intersection of skills, experiences, and insights that make someone irreplaceable. Based on Naval Ravikant's concept, specific knowledge cannot be trained for; it's learned through apprenticeships, curiosity, and unique life experiences.
Here are someone's answers to 10 questions about their specific knowledge:
${formatted}
Provide a 3-sentence analysis of what you believe their specific knowledge is. Be specific, insightful, and help them see patterns they might have missed. Focus on the unique intersection of their skills and experiences. Write in second person ("You..."). Make it feel personal and revelatory.`;
return await callAPI(prompt, 512);
}
// ============================================
// FLOW
// ============================================
function askQuestion(index) {
currentQuestion = index;
dom.status.textContent = `Question ${index + 1} of 10`;
addMessage(QUESTIONS[index], 'system');
dom.input.focus();
}
async function send() {
const text = dom.input.value.trim();
if (!text || waiting) return;
addMessage(text, 'user');
dom.input.value = '';
setWaiting(true);
history.push({ question: QUESTIONS[currentQuestion], answer: text });
// Save answer and move on
answers[currentQuestion] = text;
if (currentQuestion < 9) {
setTimeout(() => askQuestion(currentQuestion + 1), 500);
} else {
dom.status.textContent = 'Analyzing...';
dom.inputWrapper.classList.add('hidden');
showTyping();
try {
const analysis = await getAnalysis();
hideTyping();
dom.status.textContent = 'Complete';
const formatted = analysis.split('\n').filter(p => p.trim()).map(p => `<p>${p}</p>`).join('');
addMessage(`
<div class="result-section">
<strong>Your Specific Knowledge:</strong>
${formatted}
<span class="restart-link" onclick="location.reload()">Start over</span>
</div>
`, 'system');
} catch (err) {
hideTyping();
addMessage(`Error: ${err.message}. Please try again.`, 'system');
}
}
setWaiting(false);
dom.input.focus();
}
// ============================================
// INIT
// ============================================
dom.input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
});
dom.sendBtn.addEventListener('click', send);
askQuestion(0);
</script>
</body>
</html>
comments
no comments yet — be the first