Changes to mediscan1.scroll.pub

root
root
24 days ago
Initial commit
body.html
Changed around line 1
+
+
+

MediScan

+

AI-Powered Medical Prescription Analysis

+
+
+
+
+
+
+
+
+
+
+

Tap to upload or take photos

+ Support for multiple images
+
+
+
+
+
+
+
+
+
+
+

Analysis Results

+
+
+
+
+
+
+

MediScan - Making medical prescriptions easier to understand

+
index.scroll
Changed around line 1
+ buildHtml
+ baseUrl https://mediscan.scroll.pub
+ metaTags
+ editButton /edit.html
+ title MediScan - AI-Powered Medical Prescription Analysis
+ style.css
+ body.html
+ script.js
prompt.json
Changed around line 1
+ {
+ "id": "msg_01PEKNMwgv2ErURArcEz8evx",
+ "type": "message",
+ "role": "assistant",
+ "model": "claude-3-5-sonnet-20241022",
+ "content": [
+ {
+ "type": "text",
+ "text": "Let me create a website for this medical prescription analysis application.\n\n---domain---\nmediscan.scroll.pub\n\n---index.scroll---\nbuildHtml\nbaseUrl https://mediscan.scroll.pub\nmetaTags\neditButton /edit.html\ntitle MediScan - AI-Powered Medical Prescription Analysis\nstyle.css\nbody.html\nscript.js\n\n---body.html---\n
\n \n
\n\n
\n
\n
\n
\n \n
\n \n

Tap to upload or take photos

\n Support for multiple images\n
\n
\n
\n
\n \n
\n\n
\n
\n
\n

Analysis Results

\n
\n
\n
\n
\n\n
\n

MediScan - Making medical prescriptions easier to understand

\n
\n\n---style.css---\n:root {\n --primary-color: #2c7be5;\n --secondary-color: #6c757d;\n --background-color: #f8f9fa;\n --text-color: #212529;\n --border-radius: 12px;\n}\n\n* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n line-height: 1.6;\n color: var(--text-color);\n background: var(--background-color);\n}\n\nheader {\n background: white;\n box-shadow: 0 2px 10px rgba(0,0,0,0.1);\n padding: 1rem;\n}\n\nheader h1 {\n color: var(--primary-color);\n font-size: 2rem;\n}\n\nmain {\n max-width: 1200px;\n margin: 2rem auto;\n padding: 0 1rem;\n}\n\n.upload-section {\n background: white;\n border-radius: var(--border-radius);\n padding: 2rem;\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\n}\n\n.upload-box {\n border: 2px dashed var(--primary-color);\n border-radius: var(--border-radius);\n padding: 2rem;\n text-align: center;\n cursor: pointer;\n position: relative;\n transition: all 0.3s ease;\n}\n\n.upload-box:hover {\n background: rgba(44,123,229,0.05);\n}\n\n.upload-box input[type=\"file\"] {\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n opacity: 0;\n cursor: pointer;\n}\n\n.icon-upload {\n display: block;\n width: 48px;\n height: 48px;\n margin: 0 auto 1rem;\n background: var(--primary-color);\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z'/%3E%3C/svg%3E\") no-repeat center;\n -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z'/%3E%3C/svg%3E\") no-repeat center;\n}\n\n.preview-container {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n gap: 1rem;\n margin-top: 2rem;\n}\n\n.preview-image {\n width: 100%;\n height: 150px;\n object-fit: cover;\n border-radius: var(--border-radius);\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.analyze-btn {\n display: block;\n width: 100%;\n max-width: 300px;\n margin: 2rem auto 0;\n padding: 1rem;\n background: var(--primary-color);\n color: white;\n border: none;\n border-radius: var(--border-radius);\n font-size: 1.1rem;\n cursor: pointer;\n transition: all 0.3s ease;\n}\n\n.analyze-btn:disabled {\n background: var(--secondary-color);\n cursor: not-allowed;\n}\n\n.result-section {\n margin-top: 2rem;\n background: white;\n border-radius: var(--border-radius);\n padding: 2rem;\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\n display: none;\n}\n\n.loading-spinner {\n width: 48px;\n height: 48px;\n border: 4px solid var(--primary-color);\n border-top: 4px solid transparent;\n border-radius: 50%;\n margin: 2rem auto;\n animation: spin 1s linear infinite;\n display: none;\n}\n\n@keyframes spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\nfooter {\n text-align: center;\n padding: 2rem;\n background: white;\n margin-top: 4rem;\n}\n\n@media (max-width: 768px) {\n .upload-section {\n padding: 1rem;\n }\n \n .preview-container {\n grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));\n }\n \n .preview-image {\n height: 100px;\n }\n}\n\n---script.js---\nclass MediScan {\n constructor() {\n this.config = {\n photo_model: process.env.PHOTO_MODEL,\n photo_prompt: process.env.PHOTO_PROMPT,\n photo_base_url: process.env.PHOTO_BASE_URL,\n photo_api_key: process.env.PHOTO_API_KEY,\n photo_max_tokens: process.env.PHOTO_MAX_TOKENS,\n photo_temperature: process.env.PHOTO_TEMPERATURE,\n text_model: process.env.TEXT_MODEL,\n text_prompt: process.env.TEXT_PROMPT,\n text_base_url: process.env.TEXT_BASE_URL,\n text_api_key: process.env.TEXT_API_KEY,\n text_max_tokens: process.env.TEXT_MAX_TOKENS,\n text_temperature: process.env.TEXT_TEMPERATURE\n };\n \n this.selectedFiles = [];\n this.initializeElements();\n this.attachEventListeners();\n }\n\n initializeElements() {\n this.fileInput = document.getElementById('fileInput');\n this.uploadBox = document.getElementById('uploadBox');\n this.previewContainer = document.getElementById('previewContainer');\n this.analyzeBtn = document.getElementById('analyzeBtn');\n this.resultSection = document.getElementById('resultSection');\n this.loadingSpinner = document.getElementById('loadingSpinner');\n this.resultContainer = document.getElementById('resultContainer');\n this.resultContent = document.getElementById('resultContent');\n }\n\n attachEventListeners() {\n this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));\n this.analyzeBtn.addEventListener('click', () => this.analyzePrescriptions());\n }\n\n handleFileSelect(event) {\n const files = Array.from(event.target.files);\n this.selectedFiles = files;\n this.previewContainer.innerHTML = '';\n \n files.forEach(file => {\n if (file.type.startsWith('image/')) {\n const reader = new FileReader();\n reader.onload = (e) => {\n const img = document.createElement('img');\n img.src = e.target.result;\n img.classList.add('preview-image');\n this.previewContainer.appendChild(img);\n };\n reader.readAsDataURL(file);\n }\n });\n\n this.analyzeBtn.disabled = files.length === 0;\n }\n\n async analyzePrescriptions() {\n try {\n this.showLoading();\n \n // Convert images to base64\n const imagePromises = this.selectedFiles.map(file => this.fileToBase64(file));\n const base64Images = await Promise.all(imagePromises);\n \n // Process images with vision model\n const imageAnalysisPromises = base64Images.map(image => \n this.processWithVisionModel(image)\n );\n const imageResults = await Promise.all(imageAnalysisPromises);\n \n // Process results with language model\n const finalAnalysis = await this.processWithLanguageModel(imageResults);\n \n this.displayResults(finalAnalysis);\n } catch (error) {\n console.error('Analysis failed:', error);\n this.displayError('Analysis failed. Please try again.');\n } finally {\n this.hideLoading();\n }\n }\n\n fileToBase64(file) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result.split(',')[1]);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n }\n\n async processWithVisionModel(base64Image) {\n // Implementation would depend on the specific API being used\n // This is a placeholder for the actual API call\n return fetch(this.config.photo_base_url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.photo_api_key}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: this.config.photo_model,\n prompt: this.config.photo_prompt,\n image: base64Image,\n max_tokens: this.config.photo_max_tokens,\n temperature: this.config.photo_temperature\n })\n }).then(response => response.json());\n }\n\n async processWithLanguageModel(imageResults) {\n // Implementation would depend on the specific API being used\n // This is a placeholder for the actual API call\n return fetch(this.config.text_base_url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.text_api_key}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: this.config.text_model,\n prompt: this.config.text_prompt,\n messages: [{\n role: 'user',\n content: JSON.stringify(imageResults)\n }],\n max_tokens: this.config.text_max_tokens,\n temperature: this.config.text_temperature\n })\n }).then(response => response.json());\n }\n\n showLoading() {\n this.resultSection.style.display = 'block';\n this.loadingSpinner.style.display = 'block';\n this.resultContainer.style.display = 'none';\n }\n\n hideLoading() {\n this.loadingSpinner.style.display = 'none';\n this.resultContainer.style.display = 'block';\n }\n\n displayResults(analysis) {\n this.resultContent.innerHTML = `\n
\n

Prescription Analysis

\n

${analysis.result || analysis.choices[0].message.content}

\n
\n `;\n }\n\n displayError(message) {\n this.resultContent.innerHTML = `\n
\n

${message}

\n
\n `;\n }\n}\n\n// Initialize the application\ndocument.addEventListener('DOMContentLoaded', () => {\n new MediScan();\n});\n\n---end---\n\nThis implementation creates a clean, modern web application for analyzing medical prescriptions using AI. The domain name \"mediscan.scroll.pub\" is memorable and clearly indicates the purpose of the site.\n\nKey features:\n- Mobile-first responsive design\n- Drag-and-drop or click-to-upload functionality\n- Camera capture support on mobile devices\n- Multiple image upload support\n- Real-time image preview\n- Loading states and error handling\n- Clean, accessible UI\n- Environment variable configuration for AI models\n- Modular JavaScript architecture\n- Beautiful animations and transitions\n- Semantic HTML5 structure\n\nThe application uses a class-based architecture for better organization and maintainability. The CSS includes modern features like CSS variables, grid layout, and flexible responsive design. The UI is optimized for both desktop and mobile devices, with appropriate touch targets and intuitive interactions."
+ }
+ ],
+ "stop_reason": "end_turn",
+ "stop_sequence": null,
+ "usage": {
+ "input_tokens": 444,
+ "cache_creation_input_tokens": 0,
+ "cache_read_input_tokens": 0,
+ "output_tokens": 3592
+ }
+ }
readme.scroll
Changed around line 1
+ # mediscan1.scroll.pub
+ Website generated by Claude from prompt: 一个web应用,用户手机上传或者拍照几张照片,通过识图大模型读取文件上的内容,并有语言大模型和用户交流后给出一段对图片上医嘱的分析和解读。其中识图大模型和语言大模型都会使用支持openai库的第三方大模型,所以需要用环境变量管理包括:photo_model/photo_prompt/base_url/api_key/max_tokens/tempreture以及text_model/text_prompt/base_url/api_key/max_tokens/tempreture,方便管理和调试大模型。
script.js
Changed around line 1
+ class MediScan {
+ constructor() {
+ this.config = {
+ photo_model: process.env.PHOTO_MODEL,
+ photo_prompt: process.env.PHOTO_PROMPT,
+ photo_base_url: process.env.PHOTO_BASE_URL,
+ photo_api_key: process.env.PHOTO_API_KEY,
+ photo_max_tokens: process.env.PHOTO_MAX_TOKENS,
+ photo_temperature: process.env.PHOTO_TEMPERATURE,
+ text_model: process.env.TEXT_MODEL,
+ text_prompt: process.env.TEXT_PROMPT,
+ text_base_url: process.env.TEXT_BASE_URL,
+ text_api_key: process.env.TEXT_API_KEY,
+ text_max_tokens: process.env.TEXT_MAX_TOKENS,
+ text_temperature: process.env.TEXT_TEMPERATURE
+ };
+
+ this.selectedFiles = [];
+ this.initializeElements();
+ this.attachEventListeners();
+ }
+
+ initializeElements() {
+ this.fileInput = document.getElementById('fileInput');
+ this.uploadBox = document.getElementById('uploadBox');
+ this.previewContainer = document.getElementById('previewContainer');
+ this.analyzeBtn = document.getElementById('analyzeBtn');
+ this.resultSection = document.getElementById('resultSection');
+ this.loadingSpinner = document.getElementById('loadingSpinner');
+ this.resultContainer = document.getElementById('resultContainer');
+ this.resultContent = document.getElementById('resultContent');
+ }
+
+ attachEventListeners() {
+ this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
+ this.analyzeBtn.addEventListener('click', () => this.analyzePrescriptions());
+ }
+
+ handleFileSelect(event) {
+ const files = Array.from(event.target.files);
+ this.selectedFiles = files;
+ this.previewContainer.innerHTML = '';
+
+ files.forEach(file => {
+ if (file.type.startsWith('image/')) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = document.createElement('img');
+ img.src = e.target.result;
+ img.classList.add('preview-image');
+ this.previewContainer.appendChild(img);
+ };
+ reader.readAsDataURL(file);
+ }
+ });
+
+ this.analyzeBtn.disabled = files.length === 0;
+ }
+
+ async analyzePrescriptions() {
+ try {
+ this.showLoading();
+
+ // Convert images to base64
+ const imagePromises = this.selectedFiles.map(file => this.fileToBase64(file));
+ const base64Images = await Promise.all(imagePromises);
+
+ // Process images with vision model
+ const imageAnalysisPromises = base64Images.map(image =>
+ this.processWithVisionModel(image)
+ );
+ const imageResults = await Promise.all(imageAnalysisPromises);
+
+ // Process results with language model
+ const finalAnalysis = await this.processWithLanguageModel(imageResults);
+
+ this.displayResults(finalAnalysis);
+ } catch (error) {
+ console.error('Analysis failed:', error);
+ this.displayError('Analysis failed. Please try again.');
+ } finally {
+ this.hideLoading();
+ }
+ }
+
+ fileToBase64(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result.split(',')[1]);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+ }
+
+ async processWithVisionModel(base64Image) {
+ // Implementation would depend on the specific API being used
+ // This is a placeholder for the actual API call
+ return fetch(this.config.photo_base_url, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.config.photo_api_key}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: this.config.photo_model,
+ prompt: this.config.photo_prompt,
+ image: base64Image,
+ max_tokens: this.config.photo_max_tokens,
+ temperature: this.config.photo_temperature
+ })
+ }).then(response => response.json());
+ }
+
+ async processWithLanguageModel(imageResults) {
+ // Implementation would depend on the specific API being used
+ // This is a placeholder for the actual API call
+ return fetch(this.config.text_base_url, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.config.text_api_key}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: this.config.text_model,
+ prompt: this.config.text_prompt,
+ messages: [{
+ role: 'user',
+ content: JSON.stringify(imageResults)
+ }],
+ max_tokens: this.config.text_max_tokens,
+ temperature: this.config.text_temperature
+ })
+ }).then(response => response.json());
+ }
+
+ showLoading() {
+ this.resultSection.style.display = 'block';
+ this.loadingSpinner.style.display = 'block';
+ this.resultContainer.style.display = 'none';
+ }
+
+ hideLoading() {
+ this.loadingSpinner.style.display = 'none';
+ this.resultContainer.style.display = 'block';
+ }
+
+ displayResults(analysis) {
+ this.resultContent.innerHTML = `
+
+

Prescription Analysis

+

${analysis.result || analysis.choices[0].message.content}

+
+ `;
+ }
+
+ displayError(message) {
+ this.resultContent.innerHTML = `
+
+

${message}

+
+ `;
+ }
+ }
+
+ // Initialize the application
+ document.addEventListener('DOMContentLoaded', () => {
+ new MediScan();
+ });
style.css
Changed around line 1
+ :root {
+ --primary-color: #2c7be5;
+ --secondary-color: #6c757d;
+ --background-color: #f8f9fa;
+ --text-color: #212529;
+ --border-radius: 12px;
+ }
+
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ line-height: 1.6;
+ color: var(--text-color);
+ background: var(--background-color);
+ }
+
+ header {
+ background: white;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ padding: 1rem;
+ }
+
+ header h1 {
+ color: var(--primary-color);
+ font-size: 2rem;
+ }
+
+ main {
+ max-width: 1200px;
+ margin: 2rem auto;
+ padding: 0 1rem;
+ }
+
+ .upload-section {
+ background: white;
+ border-radius: var(--border-radius);
+ padding: 2rem;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ }
+
+ .upload-box {
+ border: 2px dashed var(--primary-color);
+ border-radius: var(--border-radius);
+ padding: 2rem;
+ text-align: center;
+ cursor: pointer;
+ position: relative;
+ transition: all 0.3s ease;
+ }
+
+ .upload-box:hover {
+ background: rgba(44,123,229,0.05);
+ }
+
+ .upload-box input[type="file"] {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ cursor: pointer;
+ }
+
+ .icon-upload {
+ display: block;
+ width: 48px;
+ height: 48px;
+ margin: 0 auto 1rem;
+ background: var(--primary-color);
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z'/%3E%3C/svg%3E") no-repeat center;
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z'/%3E%3C/svg%3E") no-repeat center;
+ }
+
+ .preview-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 1rem;
+ margin-top: 2rem;
+ }
+
+ .preview-image {
+ width: 100%;
+ height: 150px;
+ object-fit: cover;
+ border-radius: var(--border-radius);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ }
+
+ .analyze-btn {
+ display: block;
+ width: 100%;
+ max-width: 300px;
+ margin: 2rem auto 0;
+ padding: 1rem;
+ background: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: var(--border-radius);
+ font-size: 1.1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ }
+
+ .analyze-btn:disabled {
+ background: var(--secondary-color);
+ cursor: not-allowed;
+ }
+
+ .result-section {
+ margin-top: 2rem;
+ background: white;
+ border-radius: var(--border-radius);
+ padding: 2rem;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ display: none;
+ }
+
+ .loading-spinner {
+ width: 48px;
+ height: 48px;
+ border: 4px solid var(--primary-color);
+ border-top: 4px solid transparent;
+ border-radius: 50%;
+ margin: 2rem auto;
+ animation: spin 1s linear infinite;
+ display: none;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ footer {
+ text-align: center;
+ padding: 2rem;
+ background: white;
+ margin-top: 4rem;
+ }
+
+ @media (max-width: 768px) {
+ .upload-section {
+ padding: 1rem;
+ }
+
+ .preview-container {
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ }
+
+ .preview-image {
+ height: 100px;
+ }
+ }