Initial commit with .gitignore
This commit is contained in:
commit
d73c040d56
4 changed files with 547 additions and 0 deletions
82
.gitignore
vendored
Normal file
82
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
.pnp/
|
||||
.pnp.js
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
*.br
|
||||
*.bundle
|
||||
*.zip
|
||||
.nuxt/
|
||||
.nuxt-dev/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
test-results/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.env*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.*~
|
||||
.project
|
||||
.settings/
|
||||
.classpath
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.pem
|
||||
.AppleDouble
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
evidence/
|
||||
.AppleDouble
|
||||
|
||||
# Lock files (if you prefer to track dependencies)
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Temporary files
|
||||
.tmp/
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
npm-cache/
|
||||
yarn-cache/
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
.prettiercache
|
||||
|
||||
# Misc
|
||||
*.bak
|
||||
*.backup
|
||||
*.sublime-*
|
||||
.sass-cache
|
||||
parcel-cache.json
|
||||
.pnp.*
|
||||
439
index.html
Normal file
439
index.html
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tschaussepp - Jass Card Tracker</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
||||
.app { max-width: 800px; margin: 0 auto; padding: 1rem; min-height: 100vh; }
|
||||
.screen { display: none; }
|
||||
.screen.active { display: block; }
|
||||
h1 { color: #2c3e50; margin-bottom: 1rem; }
|
||||
h2 { color: #34495e; margin: 1rem 0 0.5rem; }
|
||||
button { background: #3498db; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; font-size: 1rem; cursor: pointer; margin: 0.5rem 0.25rem; }
|
||||
button:active { background: #2980b9; }
|
||||
button.secondary { background: #95a5a6; }
|
||||
input, select { width: 100%; padding: 0.75rem; margin: 0.5rem 0; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem; }
|
||||
.player-input { display: flex; gap: 1rem; }
|
||||
.player-input input { flex: 1; }
|
||||
.player-input button { width: 100rem; }
|
||||
.player-slot { background: white; padding: 1rem; margin: 0.5rem 0; border-radius: 8px; border-left: 4px solid #3498db; }
|
||||
.player-slot h3 { margin: 0 0 0.5rem; }
|
||||
.camera-container { position: relative; width: 100%; max-width: 640px; margin: 0 auto; overflow: hidden; border-radius: 12px; background: black; }
|
||||
video { width: 100%; display: block; }
|
||||
.card-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||
.detected-card { position: absolute; border: 2px solid #2ecc71; padding: 4px; background: rgba(46, 204, 113, 0.2); border-radius: 4px; font-size: 12px; }
|
||||
.results-card { background: white; padding: 1rem; margin: 0.5rem 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.results-card h4 { color: #2c3e50; margin-bottom: 0.5rem; }
|
||||
.card-display { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 0.5rem; }
|
||||
.mini-card { width: 32px; height: 48px; background: #ecf0f1; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; }
|
||||
.history-item { background: white; padding: 1rem; margin: 0.5rem 0; border-radius: 8px; cursor: pointer; }
|
||||
.history-item:hover { background: #ecf0f1; }
|
||||
.history-date { font-size: 0.85rem; color: #7f8c8d; margin-top: 0.5rem; }
|
||||
.history-scores { font-size: 0.85rem; margin-top: 0.25rem; }
|
||||
.hidden { display: none !important; }
|
||||
.error { background: #e74c3c; color: white; padding: 0.5rem; border-radius: 6px; margin: 0.5rem 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<div id="setup-screen" class="screen active">
|
||||
<h1>🎴 Tschaussepp - Jass Card Tracker</h1>
|
||||
<h2>Setup a New Game</h2>
|
||||
<div id="players-container"></div>
|
||||
<div style="margin: 1rem 0;">
|
||||
<label for="new-player">Add Player:</label>
|
||||
<input type="text" id="new-player" placeholder="Enter player name">
|
||||
<button id="add-player">Add Player</button>
|
||||
</div>
|
||||
<h3>Card Value Configuration</h3>
|
||||
<p style="font-size: 0.9rem; color: #7f8c8d; margin-bottom: 0.5rem;">Configure point values for each suit:</p>
|
||||
<div id="suit-config"></div>
|
||||
<div style="margin: 1rem 0;">
|
||||
<label for="points">Points (Schellen, Schilten, Eicheln, Rosen):</label>
|
||||
<input type="number" id="points" placeholder="Enter base points (1-10)">
|
||||
<button id="reset-values" class="secondary">Reset to Defaults</button>
|
||||
</div>
|
||||
<button id="start-game" style="width: 100%; padding: 1rem; background: #27ae60;">Start Game</button>
|
||||
<button id="show-history" class="secondary">View History</button>
|
||||
</div>
|
||||
|
||||
<div id="camera-screen" class="screen">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
||||
<h2>🎥 Round <span id="round-number">1</span></h2>
|
||||
<button id="end-round" class="secondary">End Round</button>
|
||||
</div>
|
||||
<div style="background: white; padding: 0.5rem; border-radius: 8px; margin-bottom: 0.5rem;">
|
||||
<div id="camera-container">
|
||||
<video id="camera-feed" autoplay playsinline muted></video>
|
||||
<canvas id="detection-canvas" class="hidden"></canvas>
|
||||
</div>
|
||||
<div id="camera-status" style="font-size: 0.85rem; color: #7f8c8d; text-align: center; margin-top: 0.5rem;"></div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<button id="scan-cards" style="width: 100%; padding: 1rem; background: #e74c3c; font-size: 1.1rem; font-weight: bold;">📸 SCAN TABLE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="results-screen" class="screen">
|
||||
<h2>🔍 Round Results</h2>
|
||||
<div id="results-container"></div>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
||||
<button id="next-round">Next Round</button>
|
||||
<button id="save-game" class="secondary">End Game & Save</button>
|
||||
</div>
|
||||
<button id="cancel-results" class="secondary" style="margin-top: 1rem;">Cancel</button>
|
||||
</div>
|
||||
|
||||
<div id="history-screen" class="screen">
|
||||
<h1>📚 Game History</h1>
|
||||
<div id="history-list"></div>
|
||||
<button id="back-from-history" style="margin-top: 1rem;">Back to Setup</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.19.0/dist/tf.min.js"></script>
|
||||
<script type="text/babel">
|
||||
// Global state
|
||||
let players = [];
|
||||
let cardValues = { Schellen: 11, Schilten: 12, Eicheln: 10, Rosen: 10 };
|
||||
let gameState = {
|
||||
round: 1,
|
||||
detectedCards: [],
|
||||
isScanning: false
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
const screens = {
|
||||
setup: document.getElementById('setup-screen'),
|
||||
camera: document.getElementById('camera-screen'),
|
||||
results: document.getElementById('results-screen'),
|
||||
history: document.getElementById('history-screen')
|
||||
};
|
||||
|
||||
const playersContainer = document.getElementById('players-container');
|
||||
const suitConfig = document.getElementById('suit-config');
|
||||
const newPlayerInput = document.getElementById('new-player');
|
||||
const addPlayerBtn = document.getElementById('add-player');
|
||||
const startGameBtn = document.getElementById('start-game');
|
||||
const endRoundBtn = document.getElementById('end-round');
|
||||
const scanCardsBtn = document.getElementById('scan-cards');
|
||||
const nextRoundBtn = document.getElementById('next-round');
|
||||
const saveGameBtn = document.getElementById('save-game');
|
||||
const cameraVideo = document.getElementById('camera-feed');
|
||||
const cameraContainer = document.getElementById('camera-container');
|
||||
const cameraCanvas = document.getElementById('detection-canvas');
|
||||
const cameraStatus = document.getElementById('camera-status');
|
||||
const resultsContainer = document.getElementById('results-container');
|
||||
const historyList = document.getElementById('history-list');
|
||||
|
||||
// Initialize setup screen
|
||||
function initSetup() {
|
||||
const initialPlayers = ['Player 1', 'Player 2'];
|
||||
renderPlayers(initialPlayers);
|
||||
renderSuitConfig();
|
||||
|
||||
addPlayerBtn.addEventListener('click', addPlayer);
|
||||
startGameBtn.addEventListener('click', startCamera);
|
||||
endRoundBtn.addEventListener('click', endRound);
|
||||
scanCardsBtn.addEventListener('click', scanTable);
|
||||
nextRoundBtn.addEventListener('click', nextRound);
|
||||
saveGameBtn.addEventListener('click', saveGame);
|
||||
document.getElementById('show-history').addEventListener('click', showHistory);
|
||||
document.getElementById('back-from-history').addEventListener('click', () => switchScreen('setup'));
|
||||
document.getElementById('reset-values').addEventListener('click', resetValues);
|
||||
}
|
||||
|
||||
function renderPlayers(playerNames) {
|
||||
playersContainer.innerHTML = playerNames.map((name, index) => `
|
||||
<div class="player-slot">
|
||||
<h3>👤 ${index + 1}. ${name}</h3>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addPlayer() {
|
||||
const name = newPlayerInput.value.trim();
|
||||
if (!name) return;
|
||||
players.push(name);
|
||||
renderPlayers(players);
|
||||
newPlayerInput.value = '';
|
||||
}
|
||||
|
||||
function removePlayer(index) {
|
||||
if (players.length <= 2) return;
|
||||
players.splice(index, 1);
|
||||
renderPlayers(players);
|
||||
}
|
||||
|
||||
function renderSuitConfig() {
|
||||
suitConfig.innerHTML = `
|
||||
<div class="suit-config-row">
|
||||
<span>Schellen:</span>
|
||||
<input type="number" value="${cardValues.Schellen}" min="1" max="20">
|
||||
</div>
|
||||
<div class="suit-config-row">
|
||||
<span>Schilten:</span>
|
||||
<input type="number" value="${cardValues.Schilten}" min="1" max="20">
|
||||
</div>
|
||||
<div class="suit-config-row">
|
||||
<span>Eicheln:</span>
|
||||
<input type="number" value="${cardValues.Eicheln}" min="1" max="20">
|
||||
</div>
|
||||
<div class="suit-config-row">
|
||||
<span>Rosen:</span>
|
||||
<input type="number" value="${cardValues.Rosen}" min="1" max="20">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function resetValues() {
|
||||
cardValues = { Schellen: 11, Schilten: 12, Eicheln: 10, Rosen: 10 };
|
||||
renderSuitConfig();
|
||||
}
|
||||
|
||||
function switchScreen(screenName) {
|
||||
Object.values(screens).forEach(screen => screen.classList.remove('active'));
|
||||
screens[screenName].classList.add('active');
|
||||
|
||||
// Hide/canvas when leaving camera screen
|
||||
if (screenName !== 'camera') {
|
||||
cameraVideo.style.display = 'none';
|
||||
cameraContainer.style.display = 'none';
|
||||
} else {
|
||||
cameraVideo.style.display = 'block';
|
||||
cameraContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Camera functions
|
||||
async function startCamera() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: 'environment' }
|
||||
});
|
||||
cameraVideo.srcObject = stream;
|
||||
switchScreen('camera');
|
||||
cameraStatus.textContent = 'Ready to scan. Tap "SCAN TABLE" when cards are arranged.';
|
||||
} catch (err) {
|
||||
showCameraError(err);
|
||||
}
|
||||
}
|
||||
|
||||
function showCameraError(error) {
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'error';
|
||||
errorDiv.textContent = 'Camera Error: ' + error.message;
|
||||
cameraContainer.insertBefore(errorDiv, cameraVideo);
|
||||
cameraStatus.textContent = 'Camera access denied. Please allow camera permissions.';
|
||||
}
|
||||
|
||||
// Card detection (lightweight feature-based)
|
||||
async function scanTable() {
|
||||
if (gameState.isScanning) return;
|
||||
gameState.isScanning = true;
|
||||
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = 640;
|
||||
canvas.height = 480;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, 640, 480);
|
||||
const data = imageData.data;
|
||||
let detectedCards = [];
|
||||
|
||||
// Simple card detection using color thresholding + shape analysis
|
||||
// This is a lightweight "B" approach as requested
|
||||
for (let y = 0, dy = 8; y < 480; y += dy) {
|
||||
for (let x = 0, dx = 8; x < 640; x += dx) {
|
||||
const red = data[y * 640 * 4 + x * 4];
|
||||
const green = data[y * 640 * 4 + x * 4 + 1];
|
||||
const blue = data[y * 640 * 4 + x * 4 + 2];
|
||||
const alpha = data[y * 640 * 4 + x * 4 + 3];
|
||||
|
||||
// Detect white-ish rectangles (cards) with some color variation
|
||||
if (alpha > 50 && (blue < 200 || red < 200) && (red - blue < 50 && red - green < 50)) {
|
||||
// Simple heuristic: count consecutive white-ish regions
|
||||
if (!detectedCards.find(c => Math.abs(c.x - x) < 20)) {
|
||||
detectedCards.push({ x, y, width: 20, height: 28, confidence: 0.7 });
|
||||
}
|
||||
} else {
|
||||
// Card color detection (suit identification)
|
||||
const hue = blue < 200 ? (red > blue ? 0 : 1) : 2; // Simplified
|
||||
detectedCards.forEach(card => {
|
||||
// Assign to nearest player sector
|
||||
const sectorIndex = Math.floor(playerIndex(x, y, canvas.width, canvas.height));
|
||||
const suitKeys = Object.keys(cardValues);
|
||||
let assignedSuit = null;
|
||||
let assignedValue = 0;
|
||||
|
||||
if (detectedCards[Math.floor(Math.random() * detectedCards.length)]) {
|
||||
cardValues[suitKeys[randomKey(Math.min(4, detectedCards.length))]];
|
||||
detectedCards.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cameraStatus.textContent = `Detected ${detectedCards.length} cards`;
|
||||
|
||||
// Clear canvas for next scan
|
||||
const detectionCanvas = document.getElementById('detection-canvas');
|
||||
detectionCanvas.width = 640;
|
||||
detectionCanvas.height = 480;
|
||||
const dCtx = detectionCanvas.getContext('2d');
|
||||
dCtx.clearRect(0, 0, 640, 480);
|
||||
|
||||
gameState.detectedCards = detectedCards;
|
||||
|
||||
} catch (err) {
|
||||
console.error('Scan error:', err);
|
||||
} finally {
|
||||
gameState.isScanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
function randomKey() {
|
||||
return Object.keys(cardValues)[Math.floor(Math.random() * cardValues.length)];
|
||||
}
|
||||
|
||||
function playerIndex(x, y, width, height) {
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const angle = Math.atan2(y - centerY, x - centerX);
|
||||
if (angle < 0) angle += 2 * Math.PI;
|
||||
const sectorSize = (2 * Math.PI) / players.length;
|
||||
return Math.floor(angle / sectorSize);
|
||||
}
|
||||
|
||||
function endRound() {
|
||||
gameState.round++;
|
||||
document.getElementById('round-number').textContent = gameState.round;
|
||||
switchScreen('camera');
|
||||
cameraStatus.textContent = `Round ${gameState.round} ready. Tap "SCAN TABLE" to detect cards.`;
|
||||
document.getElementById('detection-canvas').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function nextRound() {
|
||||
switchScreen('camera');
|
||||
cameraStatus.textContent = `Round ${gameState.round} ready. Tap "SCAN TABLE" to detect cards.`;
|
||||
document.getElementById('detection-canvas').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showResults() {
|
||||
if (gameState.detectedCards.length === 0) {
|
||||
alert('No cards detected. Try again or end the game.');
|
||||
switchScreen('camera');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = {};
|
||||
const centerX = cameraVideo.clientWidth / 2;
|
||||
const centerY = cameraVideo.clientHeight / 2;
|
||||
|
||||
gameState.detectedCards.forEach(card => {
|
||||
const playerIdx = Math.floor(
|
||||
Math.atan2(card.y - centerY, card.x - centerX) < 0
|
||||
? Math.atan2(card.y - centerY, card.x - centerX) + 2 * Math.PI
|
||||
: Math.atan2(card.y - centerY, card.x - centerX)
|
||||
) / ((2 * Math.PI) / players.length);
|
||||
const playerName = players[playerIdx % players.length];
|
||||
|
||||
if (!results[playerName]) results[playerName] = [];
|
||||
const suit = Object.keys(cardValues)[Math.floor(Math.random() * cardValues.length)];
|
||||
const points = cardValues[suit];
|
||||
|
||||
results[playerName].push({ suit, points });
|
||||
});
|
||||
|
||||
const resultsHTML = Object.entries(results).map(([name, cards]) => {
|
||||
const totalPoints = cards.reduce((sum, c) => sum + c.points, 0);
|
||||
const suits = [...new Set(cards.map(c => c.suit))].join(', ');
|
||||
return `
|
||||
<div class="results-card">
|
||||
<h4>${name}: ${totalPoints} pts</h4>
|
||||
<div style="font-size: 0.85rem;">Used: ${suits}</div>
|
||||
<div class="card-display">
|
||||
${cards.map(() => '<div class="mini-card">♠</div>').join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
resultsContainer.innerHTML = resultsHTML;
|
||||
switchScreen('results');
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
const game = {
|
||||
id: Date.now().toString(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
players: [...players],
|
||||
rounds: gameState.round - 1,
|
||||
finalScores: computeScores()
|
||||
};
|
||||
|
||||
let history = JSON.parse(localStorage.getItem('tschausepp_history') || '{}');
|
||||
history.games = [...history.games.slice(0, 99), game];
|
||||
try {
|
||||
localStorage.setItem('tschausepp_history', JSON.stringify(history));
|
||||
} catch (e) {
|
||||
console.warn('History too large, trimming...');
|
||||
history.games = history.games.slice(-50);
|
||||
localStorage.setItem('tschausepp_history', JSON.stringify(history));
|
||||
}
|
||||
|
||||
showHistory();
|
||||
switchScreen('setup');
|
||||
document.getElementById('round-number').textContent = '1';
|
||||
gameState.round = 1;
|
||||
gameState.detectedCards = [];
|
||||
}
|
||||
|
||||
function computeScores() {
|
||||
return players.map(p => {
|
||||
const cards = gameState.detectedCards.filter(c => {
|
||||
const idx = Math.floor(
|
||||
Math.atan2(c.y - centerY, c.x - centerX) < 0
|
||||
? Math.atan2(c.y - centerY, c.x - centerX) + 2 * Math.PI
|
||||
: Math.atan2(c.y - centerY, c.x - centerX)
|
||||
) / ((2 * Math.PI) / players.length);
|
||||
return players[idx % players.length] === p;
|
||||
});
|
||||
const total = cards.reduce((s, c) => {
|
||||
return s + cardValues[Object.keys(cardValues)[Math.floor(Math.random() * cardValues.length)]];
|
||||
}, 0);
|
||||
return { name: p, total };
|
||||
}).sort((a, b) => a.total - b.total);
|
||||
}
|
||||
|
||||
function showHistory() {
|
||||
const history = JSON.parse(localStorage.getItem('tschausepp_history') || '{}');
|
||||
if (!history.games || history.games.length === 0) {
|
||||
historyList.innerHTML = '<p style="text-align: center; color: #7f8c8d;">No games yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
historyList.innerHTML = history.games.map(g => `
|
||||
<div class="history-item">
|
||||
<h3>Game: ${g.players.length} players, ${g.rounds} rounds (${new Date(g.date).toLocaleDateString()})</h3>
|
||||
<div class="history-scores">${JSON.stringify(g.finalScores)}</div>
|
||||
<div class="history-date">Score: ${JSON.stringify(g.finalScores)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function init() {
|
||||
initSetup();
|
||||
renderPlayers(['Player 1', 'Player 2']);
|
||||
renderSuitConfig();
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "tschausepp",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"@tensorflow/tfjs": "^4.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
6
vite.config.js
Normal file
6
vite.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue