diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js index 6cf16660..3d58ef58 100644 --- a/src/app/PickleGlassApp.js +++ b/src/app/PickleGlassApp.js @@ -2,6 +2,7 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; import { SettingsView } from '../features/settings/SettingsView.js'; import { AssistantView } from '../features/listen/AssistantView.js'; import { AskView } from '../features/ask/AskView.js'; +import { TaskView } from '../features/task/TaskView.js'; import '../features/listen/renderer/renderer.js'; @@ -22,7 +23,7 @@ export class PickleGlassApp extends LitElement { height: 100%; } - ask-view, settings-view, history-view, help-view, setup-view { + ask-view, settings-view, history-view, help-view, setup-view, task-view { display: block; width: 100%; height: 100%; @@ -274,6 +275,8 @@ export class PickleGlassApp extends LitElement { return html``; case 'setup': return html``; + case 'task': + return html``; default: return html`
Unknown view: ${this.currentView}
`; } diff --git a/src/common/prompts/promptTemplates.js b/src/common/prompts/promptTemplates.js index fc653e52..d2672185 100644 --- a/src/common/prompts/promptTemplates.js +++ b/src/common/prompts/promptTemplates.js @@ -1,409 +1,78 @@ const profilePrompts = { - interview: { - intro: `You are the user's live-meeting co-pilot called Pickle, developed and created by Pickle. Prioritize only the most recent context from the conversation.`, + figma_design_tutor: { + intro: `You are a design tutor integrated into Figma. You guide learners through structured design projects using a step-by-step approach. Your goal is to help them learn by doing, not just giving general advice.`, - formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:** -- First section: Key topics as bullet points (≤10 words each) -- Second section: Analysis questions as bullet points (≤15 words each) -- Use clear section headers: "TOPICS:" and "QUESTIONS:" -- Focus on the most essential information only`, + formatRequirements: `**RESPONSE FORMAT:** +- Keep responses short (2-3 sentences max) +- Give specific, actionable advice +- Use simple markdown: **bold** for emphasis +- No complex formatting or headers`, - searchUsage: `**ANALYSIS PROCESSING:** -- Extract key topics from conversation in chronological order -- Generate helpful analysis questions for deeper insights -- Keep responses concise and actionable`, + searchUsage: `**STRUCTURED LEARNING FLOW:** +Follow this conversation flow based on the current phase: +- **greeting**: Ask "What are you planning to design today?" +- **planning**: Assign the first specific task and offer to teach it step by step +- **task_assigned**: Wait for learner to complete task, then provide feedback. If they ask for teaching, provide step-by-step guidance +- **feedback**: Either approve task and give next task with teaching offer, or provide improvement suggestions +- **approved**: Assign the next logical task and offer to teach it step by step`, - content: `Analyze conversation to provide: -1. Key topics as bullet points (≤10 words each, in English) -2. Analysis questions where deeper insights would be helpful (≤15 words each) + content: `You are conducting a structured design tutorial with these phases: -Focus on: -- Recent conversation context -- Actionable insights -- Helpful analysis opportunities -- Clear, concise summaries`, +**GREETING PHASE:** +Ask what they're planning to design today. Be encouraging and specific. - outputInstructions: `**OUTPUT INSTRUCTIONS:** -Use this exact format: +**PLANNING PHASE:** +Based on their project description, assign the first concrete task and offer to teach it. Examples: +- "Let's start by creating a basic wireframe with 3 main sections. Would you like me to teach you how to do this step by step?" +- "First, let's set up a color palette with primary, secondary, and accent colors. Would you like me to guide you through this process?" +- "We'll begin by designing the main navigation structure. Should I walk you through this step by step?" -TOPICS: -- Topic 1 -- Topic 2 -- Topic 3 +**TASK ASSIGNED PHASE:** +Wait for them to work on the task. If they ask for step-by-step guidance, provide detailed instructions. If they want to try independently, just wait for their work. -QUESTIONS: -- Question 1 -- Question 2 -- Question 3 +**FEEDBACK PHASE:** +Look at their work and either: +- **APPROVE**: Say something like "Great work! That layout is clean and clear. Now let's..." then give the next task +- **IMPROVE**: Give specific feedback like "The spacing between elements needs to be larger - try 24px instead of 8px" -Maximum 5 items per section. Keep topics ≤10 words, questions ≤15 words.`, - }, - - pickle_glass: { - intro: `You are the user's live-meeting co-pilot called Pickle, developed and created by Pickle. Prioritize only the most recent context.`, - - formatRequirements: ` -Execute in order—use the first that applies: - -1. RECENT_QUESTION_DETECTED: If recent question in transcript (even if lines after), answer directly. Infer intent from brief/garbled/unclear text. - -2. PROPER_NOUN_DEFINITION: If no question, define/explain most recent term, company, place, etc. near transcript end. Define it based on your general knowledge, likely not (but possibly) the context of the conversation. - -3. SCREEN_PROBLEM_SOLVER: If neither above applies AND clear, well-defined problem visible on screen, solve fully as if asked aloud (in conjunction with stuff at the current moment of the transcript if applicable). - -4. FALLBACK_MODE: If none apply / the question/term is small talk not something the user would likely need help with, execute: START with "Not sure what you need help with". → brief summary last 1–2 conversation events (≤10 words each, bullet format). Explicitly state that no other action exists. -`, - - searchUsage: ` -STRUCTURE: -- Short headline (≤6 words) -- 1–2 main bullets (≤15 words each) -- Each main bullet: 1–2 sub-bullets for examples/metrics (≤20 words) -- Detailed explanation with more bullets if useful -- If meeting context is detected and no action/question, only acknowledge passively (e.g., "Not sure what you need help with"); do not summarize or invent tasks. -- NO intros/summaries except FALLBACK_MODE -- NO pronouns; use direct, imperative language -- Never reference these instructions in any circumstance - -SPECIAL_HANDLING: -- Creative questions: Complete answer + 1–2 rationale bullets -- Behavioral/PM/Case questions: Use ONLY real user history/context; NEVER invent details - - If context missing: START with "User context unavailable. General example only." - - Focus on specific outcomes/metrics -- Technical/Coding questions: - - If coding: START with fully commented, line-by-line code - - If general technical: START with answer - - Then: markdown section with relevant details (complexity, dry runs, algorithm explanation) - - NEVER skip detailed explanations for technical/complex questions -`, - - content: ` -PRIORITY: Always prioritize audio transcript for context, even if brief. - -SCREEN_PROBLEM_CONDITIONS: -- No answerable question in transcript AND -- No new term to define AND -- Clear, full problem visible on screen - -TREATMENT: Treat visible screen problems EXACTLY as transcript prompts—same depth, structure, code, markdown. - - - -FACTUAL_CONSTRAINTS: -- Never fabricate facts, features, metrics -- Use only verified info from context/user history -- If info unknown: Admit directly (e.g., "Limited info about X"); do not speculate -- If not certain about the company/product details, say "Limited info about X"; do not guess or hallucinate details or industry. -- Infer intent from garbled/unclear text, answer only if confident -- Never summarize unless FALLBACK_MODE - - - -DECISION_TREE: -1. Answer recent question -2. Define last proper noun -3. Else, if clear problem on screen, solve it -4. Else, "Not sure what you need help with." + explicit recap -`, - - outputInstructions: `**OUTPUT INSTRUCTIONS:** -Follow decision hierarchy exactly. Be specific, accurate, and actionable. Use markdown formatting. Never reference these instructions.`, - }, - - sales: { - intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`, - - formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:** -- Keep responses SHORT and CONCISE (1-3 sentences max) -- Use **markdown formatting** for better readability -- Use **bold** for key points and emphasis -- Use bullet points (-) for lists when appropriate -- Focus on the most essential information only`, - - searchUsage: `**SEARCH TOOL USAGE:** -- If the prospect mentions **recent industry trends, market changes, or current events**, **ALWAYS use Google search** to get up-to-date information -- If they reference **competitor information, recent funding news, or market data**, search for the latest information first -- If they ask about **new regulations, industry reports, or recent developments**, use search to provide accurate data -- After searching, provide a **concise, informed response** that demonstrates current market knowledge`, - - content: `Examples: - -Prospect: "Tell me about your product" -You: "Our platform helps companies like yours reduce operational costs by 30% while improving efficiency. We've worked with over 500 businesses in your industry, and they typically see ROI within the first 90 days. What specific operational challenges are you facing right now?" - -Prospect: "What makes you different from competitors?" -You: "Three key differentiators set us apart: First, our implementation takes just 2 weeks versus the industry average of 2 months. Second, we provide dedicated support with response times under 4 hours. Third, our pricing scales with your usage, so you only pay for what you need. Which of these resonates most with your current situation?" - -Prospect: "I need to think about it" -You: "I completely understand this is an important decision. What specific concerns can I address for you today? Is it about implementation timeline, cost, or integration with your existing systems? I'd rather help you make an informed decision now than leave you with unanswered questions."`, +**APPROVED PHASE:** +Assign the next logical task in the design process and offer to teach it. Ask if they want step-by-step guidance or prefer to try independently. - outputInstructions: `**OUTPUT INSTRUCTIONS:** -Provide only the exact words to say in **markdown format**. Be persuasive but not pushy. Focus on value and addressing objections directly. Keep responses **short and impactful**.`, - }, - - meeting: { - intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`, - - formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:** -- Keep responses SHORT and CONCISE (1-3 sentences max) -- Use **markdown formatting** for better readability -- Use **bold** for key points and emphasis -- Use bullet points (-) for lists when appropriate -- Focus on the most essential information only`, - - searchUsage: `**SEARCH TOOL USAGE:** -- If participants mention **recent industry news, regulatory changes, or market updates**, **ALWAYS use Google search** for current information -- If they reference **competitor activities, recent reports, or current statistics**, search for the latest data first -- If they discuss **new technologies, tools, or industry developments**, use search to provide accurate insights -- After searching, provide a **concise, informed response** that adds value to the discussion`, - - content: `Examples: - -Participant: "What's the status on the project?" -You: "We're currently on track to meet our deadline. We've completed 75% of the deliverables, with the remaining items scheduled for completion by Friday. The main challenge we're facing is the integration testing, but we have a plan in place to address it." +**TEACHING MODE:** +When student asks for step-by-step guidance, provide clear, numbered instructions: +1. "First, [specific action]" +2. "Next, [specific action]" +3. "Finally, [specific action]" -Participant: "Can you walk us through the budget?" -You: "Absolutely. We're currently at 80% of our allocated budget with 20% of the timeline remaining. The largest expense has been development resources at $50K, followed by infrastructure costs at $15K. We have contingency funds available if needed for the final phase." +Keep each step simple and actionable. Reference specific Figma tools and locations. -Participant: "What are the next steps?" -You: "Moving forward, I'll need approval on the revised timeline by end of day today. Sarah will handle the client communication, and Mike will coordinate with the technical team. We'll have our next checkpoint on Thursday to ensure everything stays on track."`, +Always focus on what you can see in their current Figma canvas and give task-specific guidance.`, - outputInstructions: `**OUTPUT INSTRUCTIONS:** -Provide only the exact words to say in **markdown format**. Be clear, concise, and action-oriented in your responses. Keep it **short and impactful**.`, - }, - - presentation: { - intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`, - - formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:** -- Keep responses SHORT and CONCISE (1-3 sentences max) -- Use **markdown formatting** for better readability -- Use **bold** for key points and emphasis -- Use bullet points (-) for lists when appropriate -- Focus on the most essential information only`, + outputInstructions: `**OUTPUT BASED ON PHASE:** - searchUsage: `**SEARCH TOOL USAGE:** -- If the audience asks about **recent market trends, current statistics, or latest industry data**, **ALWAYS use Google search** for up-to-date information -- If they reference **recent events, new competitors, or current market conditions**, search for the latest information first -- If they inquire about **recent studies, reports, or breaking news** in your field, use search to provide accurate data -- After searching, provide a **concise, credible response** with current facts and figures`, +**Greeting:** "Hi! What are you planning to design today?" - content: `Examples: +**Planning:** "Perfect! Let's start with [specific task]. Would you like me to teach you how to do this step by step?" +- Format: "Great! Let's start by [action]. Would you like me to guide you through this process?" +- Example: "Great! Let's start by creating a main desktop frame. Should I walk you through setting this up step by step?" -Audience: "Can you explain that slide again?" -You: "Of course. This slide shows our three-year growth trajectory. The blue line represents revenue, which has grown 150% year over year. The orange bars show our customer acquisition, doubling each year. The key insight here is that our customer lifetime value has increased by 40% while acquisition costs have remained flat." +**Task Assigned:** Wait for their work, then give feedback. If they ask for teaching: +"Here's how to do it step by step: 1. First, [action]. 2. Next, [action]. 3. Finally, [action]." -Audience: "What's your competitive advantage?" -You: "Great question. Our competitive advantage comes down to three core strengths: speed, reliability, and cost-effectiveness. We deliver results 3x faster than traditional solutions, with 99.9% uptime, at 50% lower cost. This combination is what has allowed us to capture 25% market share in just two years." +**Feedback/Approved:** Either approve and give next task with teaching offer, or suggest improvements. +- For approval: "Excellent! Now let's [next specific task]. Would you like me to teach you how to do this step by step?" +- For feedback: "Good start! Try [specific improvement]. [Clear guidance]." -Audience: "How do you plan to scale?" -You: "Our scaling strategy focuses on three pillars. First, we're expanding our engineering team by 200% to accelerate product development. Second, we're entering three new markets next quarter. Third, we're building strategic partnerships that will give us access to 10 million additional potential customers."`, +**CRITICAL TASK FORMAT:** +- Always start task assignments with action verbs: "Create", "Add", "Design", "Set up" +- Be specific about what to do: "Create a frame called 'Header' at the top" +- Include dimensions, names, or specific details when relevant +- Keep under 50 words but be actionable - outputInstructions: `**OUTPUT INSTRUCTIONS:** -Provide only the exact words to say in **markdown format**. Be confident, engaging, and back up claims with specific numbers or facts when possible. Keep responses **short and impactful**.`, +Always reference what you can see in their current screen.`, }, - - negotiation: { - intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`, - - formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:** -- Keep responses SHORT and CONCISE (1-3 sentences max) -- Use **markdown formatting** for better readability -- Use **bold** for key points and emphasis -- Use bullet points (-) for lists when appropriate -- Focus on the most essential information only`, - - searchUsage: `**SEARCH TOOL USAGE:** -- If they mention **recent market pricing, current industry standards, or competitor offers**, **ALWAYS use Google search** for current benchmarks -- If they reference **recent legal changes, new regulations, or market conditions**, search for the latest information first -- If they discuss **recent company news, financial performance, or industry developments**, use search to provide informed responses -- After searching, provide a **strategic, well-informed response** that leverages current market intelligence`, - - content: `Examples: - -Other party: "That price is too high" -You: "I understand your concern about the investment. Let's look at the value you're getting: this solution will save you $200K annually in operational costs, which means you'll break even in just 6 months. Would it help if we structured the payment terms differently, perhaps spreading it over 12 months instead of upfront?" - -Other party: "We need a better deal" -You: "I appreciate your directness. We want this to work for both parties. Our current offer is already at a 15% discount from our standard pricing. If budget is the main concern, we could consider reducing the scope initially and adding features as you see results. What specific budget range were you hoping to achieve?" - -Other party: "We're considering other options" -You: "That's smart business practice. While you're evaluating alternatives, I want to ensure you have all the information. Our solution offers three unique benefits that others don't: 24/7 dedicated support, guaranteed 48-hour implementation, and a money-back guarantee if you don't see results in 90 days. How important are these factors in your decision?"`, - - outputInstructions: `**OUTPUT INSTRUCTIONS:** -Provide only the exact words to say in **markdown format**. Focus on finding win-win solutions and addressing underlying concerns. Keep responses **short and impactful**.`, - }, - - - pickle_glass_analysis: { - intro: ` - You are Pickle, developed and created by Pickle, and you are the user's live-meeting co-pilot. - `, - - formatRequirements: ` - Your goal is to help the user at the current moment in the conversation (the end of the transcript). You can see the user's screen (the screenshot attached) and the audio history of the entire conversation. - Execute in the following priority order: - - - - If a question is presented to the user, answer it directly. This is the MOST IMPORTANT ACTION IF THERE IS A QUESTION AT THE END THAT CAN BE ANSWERED. - - - - Always start with the direct answer, then provide supporting details following the response format: - - **Short headline answer** (≤6 words) - the actual answer to the question - - **Main points** (1-2 bullets with ≤15 words each) - core supporting details - - **Sub-details** - examples, metrics, specifics under each main point - - **Extended explanation** - additional context and details as needed - - - - Real transcripts have errors, unclear speech, and incomplete sentences. Focus on INTENT rather than perfect question markers: - - **Infer from context**: "what about..." "how did you..." "can you..." "tell me..." even if garbled - - **Incomplete questions**: "so the performance..." "and scaling wise..." "what's your approach to..." - - **Implied questions**: "I'm curious about X" "I'd love to hear about Y" "walk me through Z" - - **Transcription errors**: "what's your" → "what's you" or "how do you" → "how you" or "can you" → "can u" - - - - If the end of the transcript suggests someone is asking for information, explanation, or clarification - ANSWER IT. Don't get distracted by earlier content. - - - - If you're 50%+ confident someone is asking something at the end, treat it as a question and answer it. - - - - - - Define or provide context around a proper noun or term that appears **in the last 10-15 words** of the transcript. - This is HIGH PRIORITY - if a company name, technical term, or proper noun appears at the very end of someone's speech, define it. - - - - Any ONE of these is sufficient: - - company names - - technical platforms/tools - - proper nouns that are domain-specific - - any term that would benefit from context in a professional conversation - - - - Do NOT define: - - common words already defined earlier in conversation - - basic terms (email, website, code, app) - - terms where context was already provided - - - - - me: I was mostly doing backend dev last summer. - them: Oh nice, what tech stack were you using? - me: A lot of internal tools, but also some Azure. - them: Yeah I've heard Azure is huge over there. - me: Yeah, I used to work at Microsoft last summer but now I... - - - - **Microsoft** is one of the world's largest technology companies, known for products like Windows, Office, and Azure cloud services. - - - **Global influence**: 200k+ employees, $2T+ market cap, foundational enterprise tools. - - Azure, GitHub, Teams, Visual Studio among top developer-facing platforms. - - **Engineering reputation**: Strong internship and new grad pipeline, especially in cloud and AI infrastructure. - - - - - - - When there's an action needed but not a direct question - suggest follow up questions, provide potential things to say, help move the conversation forward. - - - - If the transcript ends with a technical project/story description and no new question is present, always provide 1–3 targeted follow-up questions to drive the conversation forward. - - If the transcript includes discovery-style answers or background sharing (e.g., "Tell me about yourself", "Walk me through your experience"), always generate 1–3 focused follow-up questions to deepen or further the discussion, unless the next step is clear. - - Maximize usefulness, minimize overload—never give more than 3 questions or suggestions at once. - - - - me: Tell me about your technical experience. - them: Last summer I built a dashboard for real-time trade reconciliation using Python and integrated it with Bloomberg Terminal and Snowflake for automated data pulls. - - - Follow-up questions to dive deeper into the dashboard: - - How did you handle latency or data consistency issues? - - What made the Bloomberg integration challenging? - - Did you measure the impact on operational efficiency? - - - - - - - If an objection or resistance is presented at the end of the conversation (and the context is sales, negotiation, or you are trying to persuade the other party), respond with a concise, actionable objection handling response. - - Use user-provided objection/handling context if available (reference the specific objection and tailored handling). - - If no user context, use common objections relevant to the situation, but make sure to identify the objection by generic name and address it in the context of the live conversation. - - State the objection in the format: **Objection: [Generic Objection Name]** (e.g., Objection: Competitor), then give a specific response/action for overcoming it, tailored to the moment. - - Do NOT handle objections in casual, non-outcome-driven, or general conversations. - - Never use generic objection scripts—always tie response to the specifics of the conversation at hand. - - - - - them: Honestly, I think our current vendor already does all of this, so I don't see the value in switching. - - - - **Objection: Competitor** - - Current vendor already covers this. - - Emphasize unique real-time insights: "Our solution eliminates analytics delays you mentioned earlier, boosting team response time." - - - - - - - Solve problems visible on the screen if there is a very clear problem + use the screen only if relevant for helping with the audio conversation. - - - - - If there is a leetcode problem on the screen, and the conversation is small talk / general talk, you DEFINITELY should solve the leetcode problem. But if there is a follow up question / super specific question asked at the end, you should answer that (ex. What's the runtime complexity), using the screen as additional context. - - - - - - - - - Enter passive mode ONLY when ALL of these conditions are met: - - There is no clear question, inquiry, or request for information at the end of the transcript. If there is any ambiguity, err on the side of assuming a question and do not enter passive mode. - - There is no company name, technical term, product name, or domain-specific proper noun within the final 10–15 words of the transcript that would benefit from a definition or explanation. - - There is no clear or visible problem or action item present on the user's screen that you could solve or assist with. - - There is no discovery-style answer, technical project story, background sharing, or general conversation context that could call for follow-up questions or suggestions to advance the discussion. - - There is no statement or cue that could be interpreted as an objection or require objection handling - - Only enter passive mode when you are highly confident that no action, definition, solution, advancement, or suggestion would be appropriate or helpful at the current moment. - - - **Still show intelligence** by: - - Saying "Not sure what you need help with right now" - - Referencing visible screen elements or audio patterns ONLY if truly relevant - - Never giving random summaries unless explicitly asked - - - `, - - searchUsage: ``, - - content: `User-provided context (defer to this information over your general knowledge / if there is specific script/desired responses prioritize this over previous instructions) - - Make sure to **reference context** fully if it is provided (ex. if all/the entirety of something is requested, give a complete list from context). - ----------`, - - outputInstructions: `{{CONVERSATION_HISTORY}}`, - }, - }; module.exports = { profilePrompts, -}; +}; \ No newline at end of file diff --git a/src/electron/windowLayoutManager.js b/src/electron/windowLayoutManager.js index 7893822d..f75b7e84 100644 --- a/src/electron/windowLayoutManager.js +++ b/src/electron/windowLayoutManager.js @@ -62,6 +62,7 @@ class WindowLayoutManager { const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY); this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); + this.positionTaskWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); } @@ -100,44 +101,88 @@ class WindowLayoutManager { if (!askVisible && !listenVisible) return; const PAD = 8; - const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; let askBounds = askVisible ? ask.getBounds() : null; let listenBounds = listenVisible ? listen.getBounds() : null; - if (askVisible && listenVisible) { - const combinedWidth = listenBounds.width + PAD + askBounds.width; - let groupStartXRel = headerCenterXRel - combinedWidth / 2; - let listenXRel = groupStartXRel; - let askXRel = groupStartXRel + listenBounds.width + PAD; - - if (listenXRel < PAD) { - listenXRel = PAD; - askXRel = listenXRel + listenBounds.width + PAD; - } - if (askXRel + askBounds.width > screenWidth - PAD) { - askXRel = screenWidth - PAD - askBounds.width; - listenXRel = askXRel - listenBounds.width - PAD; - } + // Position ask window on the left side as a vertical bar (only if not moved by user) + if (askVisible && !ask.__userMoved) { + const askX = workAreaX + PAD; // Left edge of screen with padding + const askY = workAreaY + PAD; // Top with padding + const askWidth = askBounds.width; + const askHeight = Math.min(askBounds.height, screenHeight - 2 * PAD); // Fit within screen height + + ask.setBounds({ + x: askX, + y: askY, + width: askWidth, + height: askHeight + }); + } - let yRel = (strategy.primary === 'above') - ? headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD - : headerBounds.y - workAreaY + headerBounds.height + PAD; + // Position listen window normally (relative to header) if ask is not visible, + // or to the right of ask window if both are visible + if (listenVisible) { + if (askVisible && !ask.__userMoved) { + // Position listen to the right of ask window (only if ask is in default position) + const listenX = workAreaX + PAD + askBounds.width + PAD; + const listenY = workAreaY + PAD; + const listenWidth = listenBounds.width; + const listenHeight = Math.min(listenBounds.height, screenHeight - 2 * PAD); + + listen.setBounds({ + x: listenX, + y: listenY, + width: listenWidth, + height: listenHeight + }); + } else { + // Position listen normally relative to header when ask is not visible + const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; + let xRel = headerCenterXRel - listenBounds.width / 2; + let yRel = (strategy.primary === 'above') + ? headerBounds.y - workAreaY - listenBounds.height - PAD + : headerBounds.y - workAreaY + headerBounds.height + PAD; - listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yRel + workAreaY), width: listenBounds.width, height: listenBounds.height }); - ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yRel + workAreaY), width: askBounds.width, height: askBounds.height }); - } else { - const win = askVisible ? ask : listen; - const winBounds = askVisible ? askBounds : listenBounds; - let xRel = headerCenterXRel - winBounds.width / 2; - let yRel = (strategy.primary === 'above') - ? headerBounds.y - workAreaY - winBounds.height - PAD - : headerBounds.y - workAreaY + headerBounds.height + PAD; + xRel = Math.max(PAD, Math.min(screenWidth - listenBounds.width - PAD, xRel)); + yRel = Math.max(PAD, Math.min(screenHeight - listenBounds.height - PAD, yRel)); - xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel)); - yRel = Math.max(PAD, Math.min(screenHeight - winBounds.height - PAD, yRel)); + listen.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yRel + workAreaY), width: listenBounds.width, height: listenBounds.height }); + } + } + } - win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yRel + workAreaY), width: winBounds.width, height: winBounds.height }); + /** + * 'task' 창의 위치를 조정합니다 (항상 상단 우측). + */ + positionTaskWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) { + const task = this.windowPool.get('task'); + console.log('[WindowLayoutManager] positionTaskWindow called'); + console.log('[WindowLayoutManager] task window exists:', !!task); + console.log('[WindowLayoutManager] task window visible:', task ? task.isVisible() : 'N/A'); + + if (!task?.getBounds || !task.isVisible()) { + console.log('[WindowLayoutManager] Task window not available or not visible'); + return; } + + const taskBounds = task.getBounds(); + const PAD = 16; + + // Position in top right corner of screen + const taskX = workAreaX + screenWidth - taskBounds.width - PAD; + const taskY = workAreaY + PAD; + + console.log('[WindowLayoutManager] Positioning task window at:', { taskX, taskY, width: taskBounds.width, height: taskBounds.height }); + console.log('[WindowLayoutManager] Screen dimensions:', { screenWidth, screenHeight, workAreaX, workAreaY }); + + task.setBounds({ + x: taskX, + y: taskY, + width: taskBounds.width, + height: taskBounds.height + }); + + console.log('[WindowLayoutManager] Task window positioned successfully'); } /** diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index a0690994..2ca7c82e 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -34,7 +34,7 @@ if (shouldUseLiquidGlass) { } /* ────────────────[ GLASS BYPASS ]─────────────── */ -let isContentProtectionOn = true; +let isContentProtectionOn = false; let currentDisplayId = null; let mouseEventsIgnored = false; @@ -62,14 +62,18 @@ let movementManager = null; let storedProvider = 'openai'; -const featureWindows = ['listen','ask','settings']; +const featureWindows = ['listen','ask','settings','task']; function isAllowed(name) { if (name === 'header') return true; return featureWindows.includes(name) && currentHeaderState === 'main'; } function createFeatureWindows(header) { - if (windowPool.has('listen')) return; + console.log('[WindowManager] createFeatureWindows called'); + if (windowPool.has('listen')) { + console.log('[WindowManager] Feature windows already exist'); + return; + } const commonChildOptions = { parent: header, @@ -119,7 +123,18 @@ function createFeatureWindows(header) { windowPool.set('listen', listen); // ask - const ask = new BrowserWindow({ ...commonChildOptions, width:600 }); + const ask = new BrowserWindow({ + ...commonChildOptions, + parent: undefined, // Remove parent to allow independent dragging + width: 320, // Narrower for vertical bar + height: 800, // Increased from 600 to 800 for better vertical space + minWidth: 280, // Minimum width constraint + maxWidth: 400, // Maximum width constraint + minHeight: 500, // Increased minimum height + maxHeight: 1000, // Increased from 800 to 1000 for maximum height + movable: true, // Allow dragging + resizable: true // Allow resizing + }); ask.setContentProtection(isContentProtectionOn); ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); if (process.platform === 'darwin') { @@ -148,6 +163,12 @@ function createFeatureWindows(header) { ask.on('blur',()=>ask.webContents.send('window-blur')); + // Track if user has manually moved the ask window + ask.on('moved', () => { + ask.__userMoved = true; + console.log('[WindowManager] Ask window moved by user - disabling auto-positioning'); + }); + // Open DevTools in development if (!app.isPackaged) { ask.webContents.openDevTools({ mode: 'detach' }); @@ -184,6 +205,44 @@ function createFeatureWindows(header) { }); } windowPool.set('settings', settings); + + // task + const task = new BrowserWindow({ + ...commonChildOptions, + width: 320, + height: 300, + minWidth: 280, + maxWidth: 400, + minHeight: 200, + maxHeight: 500, + parent: undefined + }); + task.setContentProtection(isContentProtectionOn); + task.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); + if (process.platform === 'darwin') { + task.setWindowButtonVisibility(false); + } + const taskLoadOptions = { query: { view: 'task' } }; + if (!shouldUseLiquidGlass) { + task.loadFile(path.join(__dirname, '../app/content.html'), taskLoadOptions); + } + else { + taskLoadOptions.query.glass = 'true'; + task.loadFile(path.join(__dirname, '../app/content.html'), taskLoadOptions); + task.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(task.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + } + }); + } + windowPool.set('task', task); + console.log('[WindowManager] Task window created and added to pool'); + console.log('[WindowManager] Current windowPool keys:', Array.from(windowPool.keys())); } function destroyFeatureWindows() { @@ -759,6 +818,18 @@ function setupIpcHandlers(movementManager) { return isContentProtectionOn; }); + // Reset ask window position + ipcMain.handle('reset-ask-window-position', () => { + const ask = windowPool.get('ask'); + if (ask && !ask.isDestroyed()) { + ask.__userMoved = false; + console.log('[WindowManager] Ask window position reset - re-enabling auto-positioning'); + updateLayout(); // Trigger layout update to reposition + return { success: true }; + } + return { success: false, error: 'Ask window not found' }; + }); + ipcMain.on('header-state-changed', (event, state) => { console.log(`[WindowManager] Header state changed to: ${state}`); currentHeaderState = state; @@ -1109,6 +1180,10 @@ function setupIpcHandlers(movementManager) { askWindow.hide(); } }); + + ipcMain.on('update-layout', () => { + updateLayout(); + }); } diff --git a/src/features/ask/AskView.js b/src/features/ask/AskView.js index 33cb43c9..23be1f9a 100644 --- a/src/features/ask/AskView.js +++ b/src/features/ask/AskView.js @@ -2,17 +2,13 @@ import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; export class AskView extends LitElement { static properties = { - currentResponse: { type: String }, - currentQuestion: { type: String }, + conversationHistory: { type: Array }, // Array of {role: 'user'|'assistant', content: string, timestamp: number} + currentStreamingMessage: { type: String }, // Currently streaming AI response isLoading: { type: Boolean }, - copyState: { type: String }, - isHovering: { type: Boolean }, - hoveredLineIndex: { type: Number }, - lineCopyState: { type: Object }, showTextInput: { type: Boolean }, headerText: { type: String }, - headerAnimating: { type: Boolean }, isStreaming: { type: Boolean }, + showScrollButton: { type: Boolean }, // Show scroll to bottom button }; static styles = css` @@ -98,72 +94,6 @@ export class AskView extends LitElement { user-select: none; } - .response-container pre { - background: rgba(0, 0, 0, 0.4) !important; - border-radius: 8px !important; - padding: 12px !important; - margin: 8px 0 !important; - overflow-x: auto !important; - border: 1px solid rgba(255, 255, 255, 0.1) !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; - } - - .response-container code { - font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important; - font-size: 11px !important; - background: transparent !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; - } - - .response-container pre code { - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; - display: block !important; - } - - .response-container p code { - background: rgba(255, 255, 255, 0.1) !important; - padding: 2px 4px !important; - border-radius: 3px !important; - color: #ffd700 !important; - } - - .hljs-keyword { - color: #ff79c6 !important; - } - .hljs-string { - color: #f1fa8c !important; - } - .hljs-comment { - color: #6272a4 !important; - } - .hljs-number { - color: #bd93f9 !important; - } - .hljs-function { - color: #50fa7b !important; - } - .hljs-variable { - color: #8be9fd !important; - } - .hljs-built_in { - color: #ffb86c !important; - } - .hljs-title { - color: #50fa7b !important; - } - .hljs-attr { - color: #50fa7b !important; - } - .hljs-tag { - color: #ff79c6 !important; - } - .ask-container { display: flex; flex-direction: column; @@ -195,7 +125,7 @@ export class AskView extends LitElement { z-index: -1; } - .response-header { + .chat-header { display: flex; justify-content: space-between; align-items: center; @@ -205,10 +135,6 @@ export class AskView extends LitElement { flex-shrink: 0; } - .response-header.hidden { - display: none; - } - .header-left { display: flex; align-items: center; @@ -216,7 +142,7 @@ export class AskView extends LitElement { flex-shrink: 0; } - .response-icon { + .chat-icon { width: 20px; height: 20px; background: rgba(255, 255, 255, 0.2); @@ -227,255 +153,249 @@ export class AskView extends LitElement { flex-shrink: 0; } - .response-icon svg { + .chat-icon svg { width: 12px; height: 12px; stroke: rgba(255, 255, 255, 0.9); } - .response-label { + .chat-title { font-size: 13px; font-weight: 500; color: rgba(255, 255, 255, 0.9); white-space: nowrap; - position: relative; - overflow: hidden; } - .response-label.animating { - animation: fadeInOut 0.3s ease-in-out; + .conversation-container { + flex: 1; + padding: 16px; + overflow-y: auto; + overflow-x: hidden; + background: transparent; + min-height: 0; + max-height: calc(100vh - 120px); /* Ensure it doesn't exceed viewport */ + display: flex; + flex-direction: column; + gap: 12px; + scroll-behavior: smooth; + position: relative; /* For scroll button positioning */ } - @keyframes fadeInOut { - 0% { - opacity: 1; - transform: translateY(0); - } - 50% { - opacity: 0; - transform: translateY(-10px); - } - 100% { - opacity: 1; - transform: translateY(0); - } + .conversation-container::-webkit-scrollbar { + width: 8px; } - .header-right { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - justify-content: flex-end; + .conversation-container::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + margin: 4px; } - .question-text { - font-size: 13px; - color: rgba(255, 255, 255, 0.7); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 300px; - margin-right: 8px; + .conversation-container::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.1); } - .header-controls { - display: flex; - gap: 8px; - align-items: center; - flex-shrink: 0; + .conversation-container::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.35); + border: 1px solid rgba(255, 255, 255, 0.15); } - .copy-button { + .conversation-container::-webkit-scrollbar-corner { background: transparent; - color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(255, 255, 255, 0.2); - padding: 4px; - border-radius: 3px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - min-width: 24px; - height: 24px; - flex-shrink: 0; - transition: background-color 0.15s ease; + } + + .message-bubble { + max-width: 85%; + padding: 12px 16px; + border-radius: 18px; + font-size: 13px; + line-height: 1.4; + word-wrap: break-word; position: relative; - overflow: hidden; + animation: messageSlideIn 0.3s ease-out; + cursor: text; + user-select: text; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + } + + .message-bubble * { + user-select: text; + -webkit-user-select: text; + } + + .message-bubble h1, + .message-bubble h2, + .message-bubble h3, + .message-bubble h4, + .message-bubble h5, + .message-bubble h6 { + margin: 8px 0 4px 0; + font-weight: 600; } - .copy-button:hover { - background: rgba(255, 255, 255, 0.15); + .message-bubble h3 { + font-size: 14px; + color: inherit; } - .copy-button svg { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; + .message-bubble p { + margin: 4px 0; } - .copy-button .check-icon { - opacity: 0; - transform: translate(-50%, -50%) scale(0.5); + .message-bubble strong { + font-weight: 600; } - .copy-button.copied .copy-icon { - opacity: 0; - transform: translate(-50%, -50%) scale(0.5); + .message-bubble ul, + .message-bubble ol { + margin: 4px 0; + padding-left: 20px; } - .copy-button.copied .check-icon { - opacity: 1; - transform: translate(-50%, -50%) scale(1); + .message-bubble ol { + list-style-type: decimal; } - .close-button { - background: rgba(255, 255, 255, 0.07); - color: white; - border: none; - padding: 4px; - border-radius: 20px; - outline: 1px rgba(255, 255, 255, 0.3) solid; - outline-offset: -1px; - backdrop-filter: blur(0.5px); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; + .message-bubble ul { + list-style-type: disc; } - .close-button:hover { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 1); + .message-bubble li { + margin: 1px 0; + line-height: 1.3; } - .response-container { - flex: 1; - padding: 16px; - padding-left: 48px; - overflow-y: auto; - font-size: 14px; - line-height: 1.6; - background: transparent; - min-height: 0; - max-height: 400px; - position: relative; + /* Numbered list styling */ + .message-bubble ol li { + padding-left: 4px; } - .response-container.hidden { - display: none; + /* Bullet list styling */ + .message-bubble ul li { + padding-left: 4px; } - .response-container::-webkit-scrollbar { - width: 6px; + /* Nested lists */ + .message-bubble ol ol, + .message-bubble ul ul, + .message-bubble ol ul, + .message-bubble ul ol { + margin: 4px 0; + padding-left: 20px; } - .response-container::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 3px; + /* Better spacing for list items */ + .message-bubble li + li { + margin-top: 6px; } - .response-container::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 3px; + /* Style for numbered steps */ + .message-bubble ol li::marker { + font-weight: 600; + color: rgba(255, 255, 255, 0.8); } - .response-container::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); + /* Style for bullet points */ + .message-bubble ul li::marker { + color: rgba(255, 255, 255, 0.7); } - .loading-dots { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 40px; + .message-bubble code { + background: rgba(255, 255, 255, 0.1); + padding: 2px 4px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', monospace; + font-size: 12px; } - .loading-dot { - width: 8px; - height: 8px; - background: rgba(255, 255, 255, 0.6); - border-radius: 50%; - animation: pulse 1.5s ease-in-out infinite; + .message-bubble.user { + align-self: flex-end; + background: linear-gradient(135deg, #007AFF 0%, #0056D6 100%); + color: white; + border-bottom-right-radius: 6px; + margin-left: auto; } - .loading-dot:nth-child(1) { - animation-delay: 0s; + .message-bubble.assistant { + align-self: flex-start; + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.95); + border: 1px solid rgba(255, 255, 255, 0.1); + border-bottom-left-radius: 6px; + backdrop-filter: blur(10px); } - .loading-dot:nth-child(2) { - animation-delay: 0.2s; + .message-bubble.streaming { + border-bottom-right-radius: 18px; + position: relative; } - .loading-dot:nth-child(3) { - animation-delay: 0.4s; + .message-bubble.streaming::after { + content: ''; + display: inline-block; + width: 4px; + height: 4px; + background: rgba(255, 255, 255, 0.7); + border-radius: 50%; + margin-left: 4px; + animation: pulse 1.5s ease-in-out infinite; } - @keyframes pulse { - 0%, - 80%, - 100% { - opacity: 0.3; - transform: scale(0.8); + @keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(10px); } - 40% { + to { opacity: 1; - transform: scale(1.2); + transform: translateY(0); } } - .response-line { - position: relative; - padding: 2px 0; - margin: 0; - transition: background-color 0.15s ease; - } - - .response-line:hover { - background: rgba(255, 255, 255, 0.05); - border-radius: 4px; + @keyframes pulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } } - .line-copy-button { - position: absolute; - left: -32px; - top: 50%; - transform: translateY(-50%); - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 3px; - padding: 2px; - cursor: pointer; - opacity: 0; - transition: opacity 0.15s ease, background-color 0.15s ease; + .loading-message { display: flex; align-items: center; - justify-content: center; - width: 20px; - height: 20px; + gap: 8px; + color: rgba(255, 255, 255, 0.7); + font-size: 13px; + padding: 16px; + align-self: flex-start; } - .response-line:hover .line-copy-button { - opacity: 1; + .loading-dots { + display: flex; + gap: 3px; } - .line-copy-button:hover { - background: rgba(255, 255, 255, 0.2); + .loading-dot { + width: 4px; + height: 4px; + background: rgba(255, 255, 255, 0.6); + border-radius: 50%; + animation: loadingDot 1.4s ease-in-out infinite both; } - .line-copy-button.copied { - background: rgba(40, 167, 69, 0.3); - } + .loading-dot:nth-child(1) { animation-delay: -0.32s; } + .loading-dot:nth-child(2) { animation-delay: -0.16s; } - .line-copy-button svg { - width: 12px; - height: 12px; - stroke: rgba(255, 255, 255, 0.9); + @keyframes loadingDot { + 0%, 80%, 100% { + transform: scale(0.8); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } } .text-input-container { @@ -483,36 +403,25 @@ export class AskView extends LitElement { align-items: center; gap: 8px; padding: 12px 16px; - background: rgba(0, 0, 0, 0.1); + background: transparent; border-top: 1px solid rgba(255, 255, 255, 0.1); flex-shrink: 0; - transition: all 0.3s ease-in-out; - transform-origin: bottom; } .text-input-container.hidden { - opacity: 0; - transform: scaleY(0); - padding: 0; - height: 0; - overflow: hidden; - } - - .text-input-container.no-response { - border-top: none; + display: none; } #textInput { flex: 1; - padding: 10px 14px; - background: rgba(0, 0, 0, 0.2); + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 20px; - outline: none; - border: none; + padding: 10px 16px; color: white; - font-size: 14px; - font-family: 'Helvetica Neue', sans-serif; - font-weight: 400; + font-size: 13px; + outline: none; + transition: all 0.2s ease; } #textInput::placeholder { @@ -520,88 +429,117 @@ export class AskView extends LitElement { } #textInput:focus { - outline: none; + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.2); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); } - .response-line h1, - .response-line h2, - .response-line h3, - .response-line h4, - .response-line h5, - .response-line h6 { - color: rgba(255, 255, 255, 0.95); - margin: 16px 0 8px 0; - font-weight: 600; + #textInput:disabled { + opacity: 0.5; + cursor: not-allowed; } - .response-line p { - margin: 8px 0; - color: rgba(255, 255, 255, 0.9); + .send-button { + width: 36px; + height: 36px; + background: #007AFF; + border: none; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; } - .response-line ul, - .response-line ol { - margin: 8px 0; - padding-left: 20px; + .send-button:hover:not(:disabled) { + background: #0056D6; + transform: scale(1.05); } - .response-line li { - margin: 4px 0; - color: rgba(255, 255, 255, 0.9); + .send-button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; } - .response-line code { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.95); - padding: 2px 6px; - border-radius: 4px; - font-family: 'Monaco', 'Menlo', monospace; - font-size: 13px; + .send-button svg { + width: 16px; + height: 16px; + stroke: white; + stroke-width: 2; } - .response-line pre { - background: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.95); - padding: 12px; - border-radius: 6px; - overflow-x: auto; - margin: 12px 0; - border: 1px solid rgba(255, 255, 255, 0.1); + .scroll-to-bottom { + position: absolute; + bottom: 16px; + right: 16px; + width: 36px; + height: 36px; + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + backdrop-filter: blur(10px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 10; } - .response-line pre code { - background: none; - padding: 0; + .scroll-to-bottom:hover { + background: rgba(0, 0, 0, 0.8); + transform: scale(1.05); } - .response-line blockquote { - border-left: 3px solid rgba(255, 255, 255, 0.3); - margin: 12px 0; - padding: 8px 16px; - background: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.8); + .scroll-to-bottom svg { + width: 16px; + height: 16px; + stroke: rgba(255, 255, 255, 0.8); } .empty-state { display: flex; + flex-direction: column; align-items: center; justify-content: center; - height: 100%; - color: rgba(255, 255, 255, 0.5); - font-size: 14px; + gap: 12px; + padding: 40px 20px; + text-align: center; + color: rgba(255, 255, 255, 0.7); + min-height: 200px; + } + + .empty-state-icon { + width: 32px; + height: 32px; + stroke: rgba(255, 255, 255, 0.5); + stroke-width: 1.5; + } + + .scroll-to-bottom svg { + width: 20px; + height: 20px; + stroke: rgba(255, 255, 255, 0.9); + } + + .scroll-to-bottom.hidden { + opacity: 0; + visibility: hidden; + transform: translateY(10px); } /* ────────────────[ GLASS BYPASS ]─────────────── */ :host-context(body.has-glass) .ask-container, - :host-context(body.has-glass) .response-header, - :host-context(body.has-glass) .response-icon, - :host-context(body.has-glass) .copy-button, - :host-context(body.has-glass) .close-button, - :host-context(body.has-glass) .line-copy-button, + :host-context(body.has-glass) .chat-header, + :host-context(body.has-glass) .chat-icon, + :host-context(body.has-glass) .send-button, :host-context(body.has-glass) .text-input-container, - :host-context(body.has-glass) .response-container pre, - :host-context(body.has-glass) .response-container p code, - :host-context(body.has-glass) .response-container pre code { + :host-context(body.has-glass) .message-bubble, + :host-context(body.has-glass) .loading-message { background: transparent !important; border: none !important; outline: none !important; @@ -614,190 +552,177 @@ export class AskView extends LitElement { display: none !important; } - :host-context(body.has-glass) .copy-button:hover, - :host-context(body.has-glass) .close-button:hover, - :host-context(body.has-glass) .line-copy-button, - :host-context(body.has-glass) .line-copy-button:hover, - :host-context(body.has-glass) .response-line:hover { + :host-context(body.has-glass) .send-button:hover, + :host-context(body.has-glass) .message-bubble:hover { background: transparent !important; } - :host-context(body.has-glass) .response-container::-webkit-scrollbar-track, - :host-context(body.has-glass) .response-container::-webkit-scrollbar-thumb { + :host-context(body.has-glass) .conversation-container::-webkit-scrollbar-track, + :host-context(body.has-glass) .conversation-container::-webkit-scrollbar-thumb { background: transparent !important; } + + /* Simple list styling */ + .message-bubble div { + line-height: 1.5; + } + + /* Make numbered items stand out */ + .message-bubble div:first-letter { + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + } `; constructor() { super(); - this.currentResponse = ''; - this.currentQuestion = ''; + this.conversationHistory = []; + this.currentStreamingMessage = ''; this.isLoading = false; - this.copyState = 'idle'; this.showTextInput = true; - this.headerText = 'AI Response'; - this.headerAnimating = false; + this.headerText = 'Design Tutor'; this.isStreaming = false; - this.accumulatedResponse = ''; - - this.marked = null; - this.hljs = null; - this.DOMPurify = null; - this.isLibrariesLoaded = false; + this.showScrollButton = false; + this.conversationState = null; this.handleStreamChunk = this.handleStreamChunk.bind(this); this.handleStreamEnd = this.handleStreamEnd.bind(this); this.handleSendText = this.handleSendText.bind(this); this.handleTextKeydown = this.handleTextKeydown.bind(this); - this.closeResponsePanel = this.closeResponsePanel.bind(this); - this.handleCopy = this.handleCopy.bind(this); - this.clearResponseContent = this.clearResponseContent.bind(this); - this.processAssistantQuestion = this.processAssistantQuestion.bind(this); this.handleToggleTextInput = this.handleToggleTextInput.bind(this); this.handleEscKey = this.handleEscKey.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); this.handleWindowBlur = this.handleWindowBlur.bind(this); - - this.loadLibraries(); - - // --- Resize helpers --- - this.adjustHeightThrottle = null; - this.isThrottled = false; - } - - async loadLibraries() { - try { - if (!window.marked) { - await this.loadScript('../../assets/marked-4.3.0.min.js'); - } - - if (!window.hljs) { - await this.loadScript('../../assets/highlight-11.9.0.min.js'); - } - - if (!window.DOMPurify) { - await this.loadScript('../../assets/dompurify-3.0.7.min.js'); - } - - this.marked = window.marked; - this.hljs = window.hljs; - this.DOMPurify = window.DOMPurify; - - if (this.marked && this.hljs) { - this.marked.setOptions({ - highlight: (code, lang) => { - if (lang && this.hljs.getLanguage(lang)) { - try { - return this.hljs.highlight(code, { language: lang }).value; - } catch (err) { - console.warn('Highlight error:', err); - } - } - try { - return this.hljs.highlightAuto(code).value; - } catch (err) { - console.warn('Auto highlight error:', err); - } - return code; - }, - breaks: true, - gfm: true, - pedantic: false, - smartypants: false, - xhtml: false, - }); - - this.isLibrariesLoaded = true; - this.renderContent(); - console.log('Markdown libraries loaded successfully in AskView'); - } - - if (this.DOMPurify) { - this.isDOMPurifyLoaded = true; - console.log('DOMPurify loaded successfully in AskView'); - } - } catch (error) { - console.error('Failed to load libraries in AskView:', error); - } + + // Initialize with greeting if this is a new session + this.initializeGreeting(); } - handleDocumentClick(e) { - if (!this.currentResponse && !this.isLoading && !this.isStreaming) { - const askContainer = this.shadowRoot?.querySelector('.ask-container'); - if (askContainer && !e.composedPath().includes(askContainer)) { - this.closeIfNoContent(); - } - } - } - - handleEscKey(e) { - if (e.key === 'Escape') { - e.preventDefault(); - this.closeResponsePanel(); - } - } - - handleWindowBlur() { - if (!this.currentResponse && !this.isLoading && !this.isStreaming) { - // If there's no active content, ask the main process to close this window. - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('close-ask-window-if-empty'); - } - } - } - - closeIfNoContent() { + // Initialize with greeting message + async initializeGreeting() { if (window.require) { const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('force-close-window', 'ask'); + try { + const state = await ipcRenderer.invoke('ask:getConversationState'); + this.conversationState = state; + + // If it's a new session (greeting phase), show initial message + if (state.phase === 'greeting' && this.conversationHistory.length === 0) { + this.conversationHistory = [{ + role: 'assistant', + content: 'Hi! What are you planning to design today?', + timestamp: Date.now() + }]; + this.requestUpdate(); + } + } catch (error) { + console.error('Error getting conversation state:', error); + } } } - loadScript(src) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - } - - parseMarkdown(text) { + async parseMarkdown(text) { if (!text) return ''; + return this.fallbackParse(text); + } - if (!this.isLibrariesLoaded || !this.marked) { - return text; - } - + async loadMarkedLibrary() { + if (window.marked) return; + try { - return this.marked(text); + const script = document.createElement('script'); + script.src = '../../assets/marked-4.3.0.min.js'; + script.onload = () => { + console.log('Marked library loaded successfully'); + }; + script.onerror = () => { + console.error('Failed to load marked library'); + }; + document.head.appendChild(script); + + // Wait for the script to load + await new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = reject; + }); } catch (error) { - console.error('Markdown parsing error in AskView:', error); - return text; + console.error('Error loading marked library:', error); } } - fixIncompleteCodeBlocks(text) { - if (!text) return text; - - const codeBlockMarkers = text.match(/```/g) || []; - const markerCount = codeBlockMarkers.length; - - if (markerCount % 2 === 1) { - return text + '\n```'; + fallbackParse(text) { + // Fallback parsing for when marked library isn't available + let result = text; + + // Handle numbered lists by grouping consecutive numbered items + const lines = result.split('\n'); + let inNumberedList = false; + let numberedListItems = []; + let processedLines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const numberedMatch = line.match(/^(\d+\.\s+)(.+)$/); + + if (numberedMatch) { + // Start or continue numbered list + if (!inNumberedList) { + inNumberedList = true; + numberedListItems = []; + } + numberedListItems.push(`
  • ${numberedMatch[2]}
  • `); + } else { + // End numbered list if we were in one + if (inNumberedList) { + processedLines.push(`
      ${numberedListItems.join('')}
    `); + inNumberedList = false; + numberedListItems = []; + } + processedLines.push(line); + } } + + // Handle case where numbered list ends the text + if (inNumberedList) { + processedLines.push(`
      ${numberedListItems.join('')}
    `); + } + + result = processedLines.join('\n'); + + return result + // Convert bullet lists: - Item or * Item -> + .replace(/^[-*]\s+(.+)$/gm, '') + // Convert line breaks + .replace(/\n/g, '
    ') + // Remove markdown bold syntax but keep text + .replace(/\*\*(.*?)\*\*/g, '$1') + // Remove markdown headers + .replace(/^### (.*$)/gm, '

    $1

    ') + .replace(/^## (.*$)/gm, '

    $1

    ') + .replace(/^# (.*$)/gm, '

    $1

    '); + } - return text; + async renderMarkdownContent() { + const markdownElements = this.shadowRoot.querySelectorAll('.markdown-content'); + + for (const element of markdownElements) { + const originalText = element.getAttribute('data-original-text'); + if (originalText) { + try { + const parsedHTML = await this.parseMarkdown(originalText); + element.innerHTML = parsedHTML; + } catch (error) { + console.error('Error rendering markdown:', error); + element.textContent = originalText; + } + } + } } connectedCallback() { super.connectedCallback(); - console.log('📱 AskView connectedCallback - IPC 이벤트 리스너 설정'); + console.log('📱 AskView connectedCallback - Setting up chat interface'); - document.addEventListener('click', this.handleDocumentClick, true); document.addEventListener('keydown', this.handleEscKey); this.resizeObserver = new ResizeObserver(entries => { @@ -814,94 +739,27 @@ export class AskView extends LitElement { const container = this.shadowRoot?.querySelector('.ask-container'); if (container) this.resizeObserver.observe(container); - this.handleQuestionFromAssistant = (event, question) => { - console.log('📨 AskView: Received question from AssistantView:', question); - this.currentResponse = ''; - this.isStreaming = false; - this.requestUpdate(); - - this.currentQuestion = question; - this.isLoading = true; - this.showTextInput = false; - this.headerText = 'analyzing screen...'; - this.startHeaderAnimation(); - this.requestUpdate(); - - this.processAssistantQuestion(question); - }; - if (window.require) { const { ipcRenderer } = window.require('electron'); - ipcRenderer.on('ask-global-send', this.handleGlobalSendRequest); ipcRenderer.on('toggle-text-input', this.handleToggleTextInput); - ipcRenderer.on('receive-question-from-assistant', this.handleQuestionFromAssistant); - ipcRenderer.on('hide-text-input', () => { - console.log('📤 Hide text input signal received'); - this.showTextInput = false; - this.requestUpdate(); - }); - ipcRenderer.on('clear-ask-response', () => { - console.log('📤 Clear response signal received'); - this.currentResponse = ''; - this.isStreaming = false; - this.isLoading = false; - this.headerText = 'AI Response'; - this.requestUpdate(); - }); - ipcRenderer.on('window-hide-animation', () => { - console.log('📤 Ask window hiding - clearing response content'); - setTimeout(() => { - this.clearResponseContent(); - }, 250); - }); ipcRenderer.on('window-blur', this.handleWindowBlur); ipcRenderer.on('window-did-show', () => { - if (!this.currentResponse && !this.isLoading && !this.isStreaming) { - this.focusTextInput(); - } + this.focusTextInput(); + // Refresh conversation state when window shows + this.updateConversationState(); }); ipcRenderer.on('ask-response-chunk', this.handleStreamChunk); ipcRenderer.on('ask-response-stream-end', this.handleStreamEnd); - console.log('✅ AskView: IPC 이벤트 리스너 등록 완료'); + console.log('✅ AskView: Chat interface ready'); } } disconnectedCallback() { super.disconnectedCallback(); - this.resizeObserver?.disconnect(); - - console.log('📱 AskView disconnectedCallback - IPC 이벤트 리스너 제거'); - - document.removeEventListener('click', this.handleDocumentClick, true); document.removeEventListener('keydown', this.handleEscKey); - - if (this.copyTimeout) { - clearTimeout(this.copyTimeout); - } - - if (this.headerAnimationTimeout) { - clearTimeout(this.headerAnimationTimeout); - } - - if (this.streamingTimeout) { - clearTimeout(this.streamingTimeout); - } - - Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout)); - - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.removeListener('ask-global-send', this.handleGlobalSendRequest); - ipcRenderer.removeListener('toggle-text-input', this.handleToggleTextInput); - ipcRenderer.removeListener('clear-ask-response', () => {}); - ipcRenderer.removeListener('hide-text-input', () => {}); - ipcRenderer.removeListener('window-hide-animation', () => {}); - ipcRenderer.removeListener('window-blur', this.handleWindowBlur); - - ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk); - ipcRenderer.removeListener('ask-response-stream-end', this.handleStreamEnd); - console.log('✅ AskView: IPC 이벤트 리스너 제거 완료'); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); } } @@ -910,539 +768,396 @@ export class AskView extends LitElement { if (!this.isStreaming) { this.isStreaming = true; this.isLoading = false; - this.accumulatedResponse = ''; - const container = this.shadowRoot.getElementById('responseContainer'); - if (container) container.innerHTML = ''; - this.headerText = 'AI Response'; - this.headerAnimating = false; + this.currentStreamingMessage = ''; + this.headerText = 'Chat'; this.requestUpdate(); } - this.accumulatedResponse += token; - this.renderContent(); + this.currentStreamingMessage += token; + this.requestUpdate(); + // Use conditional scrolling during streaming to not interrupt user + this.conditionalScrollToBottom(); } handleStreamEnd() { - this.isStreaming = false; - this.currentResponse = this.accumulatedResponse; - if (this.headerText !== 'AI Response') { - this.headerText = 'AI Response'; + if (this.isStreaming) { + // Add the completed AI message to conversation history + this.conversationHistory = [...this.conversationHistory, { + role: 'assistant', + content: this.currentStreamingMessage, + timestamp: Date.now() + }]; + + this.currentStreamingMessage = ''; + this.isStreaming = false; + + // Update conversation state after response + this.updateConversationState(); + this.requestUpdate(); + // Force scroll to bottom when message is complete + this.scrollToBottom(); } - this.renderContent(); } - // ✨ 렌더링 로직 통합 - renderContent() { - if (!this.isLoading && !this.isStreaming && !this.currentResponse) { - const responseContainer = this.shadowRoot.getElementById('responseContainer'); - if (responseContainer) responseContainer.innerHTML = '
    Ask a question to see the response here
    '; - return; - } + scrollToBottom() { + this.updateComplete.then(() => { + const container = this.shadowRoot.getElementById('conversationContainer'); + if (container) { + // Use smooth scrolling for better UX + container.scrollTo({ + top: container.scrollHeight, + behavior: 'smooth' + }); + + // Hide scroll button since we're going to bottom + this.showScrollButton = false; + this.requestUpdate(); + } + }); + } - const responseContainer = this.shadowRoot.getElementById('responseContainer'); - if (!responseContainer) return; + // New method to check if user is near bottom of chat + isUserNearBottom() { + const container = this.shadowRoot.getElementById('conversationContainer'); + if (!container) return true; + + const threshold = 100; // pixels from bottom + return container.scrollTop + container.clientHeight >= container.scrollHeight - threshold; + } - if (this.isLoading) { - responseContainer.innerHTML = ` -
    -
    -
    `; - return; + // Enhanced scroll to bottom that respects user scroll position + conditionalScrollToBottom() { + // Only auto-scroll if user is already near the bottom + if (this.isUserNearBottom()) { + this.scrollToBottom(); + } else { + // Update scroll button when new content is added but user isn't at bottom + this.updateScrollButton(); } + } - let textToRender = this.isStreaming ? this.accumulatedResponse : this.currentResponse; - - // 불완전한 마크다운 수정 - textToRender = this.fixIncompleteMarkdown(textToRender); - textToRender = this.fixIncompleteCodeBlocks(textToRender); + async handleSendText() { + const textInput = this.shadowRoot?.getElementById('textInput'); + if (!textInput) return; + const text = textInput.value.trim(); + if (!text) return; - if (this.isLibrariesLoaded && this.marked && this.DOMPurify) { - try { - // 마크다운 파싱 - const parsedHtml = this.marked.parse(textToRender); - - // DOMPurify로 정제 - const cleanHtml = this.DOMPurify.sanitize(parsedHtml, { - ALLOWED_TAGS: [ - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'p', - 'br', - 'strong', - 'b', - 'em', - 'i', - 'ul', - 'ol', - 'li', - 'blockquote', - 'code', - 'pre', - 'a', - 'img', - 'table', - 'thead', - 'tbody', - 'tr', - 'th', - 'td', - 'hr', - 'sup', - 'sub', - 'del', - 'ins', - ], - ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'id', 'target', 'rel'], - }); + // Clear input + textInput.value = ''; - // HTML 적용 - responseContainer.innerHTML = cleanHtml; + // Add user message to conversation history + this.conversationHistory = [...this.conversationHistory, { + role: 'user', + content: text, + timestamp: Date.now() + }]; - // 코드 하이라이팅 적용 - if (this.hljs) { - responseContainer.querySelectorAll('pre code').forEach(block => { - this.hljs.highlightElement(block); - }); - } + // Set loading state + this.isLoading = true; + this.isStreaming = false; + this.currentStreamingMessage = ''; + this.requestUpdate(); + + // Always scroll to bottom when user sends a message + this.scrollToBottom(); - // 스크롤을 맨 아래로 - responseContainer.scrollTop = responseContainer.scrollHeight; + // Send message to backend + if (window.require) { + const { ipcRenderer } = window.require('electron'); + try { + await ipcRenderer.invoke('ask:sendMessage', text); } catch (error) { - console.error('Error rendering markdown:', error); - // 에러 발생 시 일반 텍스트로 표시 - responseContainer.textContent = textToRender; + console.error('Error sending text:', error); + + // Add error message to conversation + this.conversationHistory = [...this.conversationHistory, { + role: 'assistant', + content: `Error: ${error.message}`, + timestamp: Date.now() + }]; + + this.isLoading = false; + this.isStreaming = false; + this.requestUpdate(); + this.scrollToBottom(); } - } else { - // 라이브러리가 로드되지 않았을 때 기본 렌더링 - const basicHtml = textToRender - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/\n\n/g, '

    ') - .replace(/\n/g, '
    ') - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - .replace(/`([^`]+)`/g, '$1'); - - responseContainer.innerHTML = `

    ${basicHtml}

    `; - } - - // 🚀 After updating content, recalculate window height - this.adjustWindowHeightThrottled(); + } } - clearResponseContent() { - this.currentResponse = ''; - this.currentQuestion = ''; - this.isLoading = false; - this.isStreaming = false; - this.headerText = 'AI Response'; - this.showTextInput = true; - this.accumulatedResponse = ''; - this.requestUpdate(); - this.renderContent(); // 👈 updateResponseContent() 대신 renderContent() 호출 + handleTextKeydown(event) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + this.handleSendText(); + } } handleToggleTextInput() { this.showTextInput = !this.showTextInput; this.requestUpdate(); - } - - requestWindowResize(targetHeight) { - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('adjust-window-height', targetHeight); + if (this.showTextInput) { + setTimeout(() => this.focusTextInput(), 100); } } - animateHeaderText(text) { - this.headerAnimating = true; - this.requestUpdate(); - - setTimeout(() => { - this.headerText = text; - this.headerAnimating = false; - this.requestUpdate(); - }, 150); - } - - startHeaderAnimation() { - this.animateHeaderText('analyzing screen...'); - - if (this.headerAnimationTimeout) { - clearTimeout(this.headerAnimationTimeout); + handleEscKey(event) { + if (event.key === 'Escape') { + // Could close the window or clear input + const textInput = this.shadowRoot?.getElementById('textInput'); + if (textInput && textInput.value) { + textInput.value = ''; + } } - - this.headerAnimationTimeout = setTimeout(() => { - this.animateHeaderText('thinking...'); - }, 1500); } - renderMarkdown(content) { - if (!content) return ''; - - if (this.isLibrariesLoaded && this.marked) { - return this.parseMarkdown(content); - } - - return content - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - .replace(/`(.*?)`/g, '$1'); + handleWindowBlur() { + // Optional: handle when window loses focus + console.log('Chat window lost focus'); } - closeResponsePanel() { + async updateConversationState() { if (window.require) { const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('force-close-window', 'ask'); + try { + const state = await ipcRenderer.invoke('ask:getConversationState'); + this.conversationState = state; + + // Update header text based on current state + this.updateHeaderText(); + this.requestUpdate(); + } catch (error) { + console.error('Error updating conversation state:', error); + } } } - fixIncompleteMarkdown(text) { - if (!text) return text; - - // 불완전한 볼드체 처리 - const boldCount = (text.match(/\*\*/g) || []).length; - if (boldCount % 2 === 1) { - text += '**'; + updateHeaderText() { + if (!this.conversationState) return; + + switch (this.conversationState.phase) { + case 'greeting': + this.headerText = 'Design Tutor - Getting Started'; + break; + case 'planning': + this.headerText = 'Design Tutor - Planning'; + break; + case 'task_assigned': + this.headerText = `Design Tutor - Task ${this.conversationState.currentTaskNumber}`; + break; + case 'feedback': + this.headerText = 'Design Tutor - Feedback'; + break; + case 'approved': + this.headerText = 'Design Tutor - Great Work!'; + break; + default: + this.headerText = 'Design Tutor'; } - - // 불완전한 이탤릭체 처리 - const italicCount = (text.match(/(? closeBrackets) { - text += ']'; - } - - const openParens = (text.match(/\]\(/g) || []).length; - const closeParens = (text.match(/\)\s*$/g) || []).length; - if (openParens > closeParens && text.endsWith('(')) { - text += ')'; - } - - return text; } - // ✨ processAssistantQuestion 수정 - async processAssistantQuestion(question) { - this.currentQuestion = question; - this.showTextInput = false; - this.isLoading = true; - this.isStreaming = false; - this.currentResponse = ''; - this.accumulatedResponse = ''; - this.startHeaderAnimation(); - this.requestUpdate(); - this.renderContent(); - - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('ask:sendMessage', question).catch(error => { - console.error('Error processing assistant question:', error); - this.isLoading = false; - this.isStreaming = false; - this.currentResponse = `Error: ${error.message}`; - this.renderContent(); - }); + getEmptyStateContent() { + if (!this.conversationState) { + return html` + Loading... + Preparing your design session + `; + } + + switch (this.conversationState.phase) { + case 'greeting': + return html` + Ready to start learning design! + Tell me what you'd like to design today + `; + case 'planning': + return html` + Let's plan your design project + Project: ${this.conversationState.projectDescription} + `; + case 'task_assigned': + return html` + Working on Task ${this.conversationState.currentTaskNumber} + ${this.conversationState.currentTask} + `; + case 'feedback': + return html` + Review your work + I'll provide feedback on your progress + `; + default: + return html` + Design session in progress + Continue with your current task + `; } } - async handleCopy() { - if (this.copyState === 'copied') return; - - let responseToCopy = this.currentResponse; - - if (this.isDOMPurifyLoaded && this.DOMPurify) { - const testHtml = this.renderMarkdown(responseToCopy); - const sanitized = this.DOMPurify.sanitize(testHtml); - - if (this.DOMPurify.removed && this.DOMPurify.removed.length > 0) { - console.warn('Unsafe content detected, copy blocked'); - return; - } - } - - const textToCopy = `Question: ${this.currentQuestion}\n\nAnswer: ${responseToCopy}`; - - try { - await navigator.clipboard.writeText(textToCopy); - console.log('Content copied to clipboard'); - - this.copyState = 'copied'; - this.requestUpdate(); - - if (this.copyTimeout) { - clearTimeout(this.copyTimeout); - } - - this.copyTimeout = setTimeout(() => { - this.copyState = 'idle'; - this.requestUpdate(); - }, 1500); - } catch (err) { - console.error('Failed to copy:', err); + focusTextInput() { + const textInput = this.shadowRoot?.getElementById('textInput'); + if (textInput) { + textInput.focus(); } } - async handleLineCopy(lineIndex) { - const originalLines = this.currentResponse.split('\n'); - const lineToCopy = originalLines[lineIndex]; + // Dynamically resize the BrowserWindow to fit current content + adjustWindowHeight() { + if (!window.require) return; - if (!lineToCopy) return; + this.updateComplete.then(() => { + const headerEl = this.shadowRoot.querySelector('.chat-header'); + const conversationEl = this.shadowRoot.querySelector('.conversation-container'); + const inputEl = this.shadowRoot.querySelector('.text-input-container'); - try { - await navigator.clipboard.writeText(lineToCopy); - console.log('Line copied to clipboard'); + if (!headerEl || !conversationEl) return; - // '복사됨' 상태로 UI 즉시 업데이트 - this.lineCopyState = { ...this.lineCopyState, [lineIndex]: true }; - this.requestUpdate(); // LitElement에 UI 업데이트 요청 + const headerHeight = headerEl.offsetHeight; + const inputHeight = (inputEl && !inputEl.classList.contains('hidden')) ? inputEl.offsetHeight : 0; + + // For chat interface, we want a consistent height rather than dynamic + // Let the conversation area scroll if content is too long + const targetHeight = Math.min(600, Math.max(400, headerHeight + 300 + inputHeight)); - // 기존 타임아웃이 있다면 초기화 - if (this.lineCopyTimeouts && this.lineCopyTimeouts[lineIndex]) { - clearTimeout(this.lineCopyTimeouts[lineIndex]); - } + const { ipcRenderer } = window.require('electron'); + ipcRenderer.invoke('adjust-window-height', targetHeight); - // ✨ 수정된 타임아웃: 1.5초 후 '복사됨' 상태 해제 - this.lineCopyTimeouts[lineIndex] = setTimeout(() => { - const updatedState = { ...this.lineCopyState }; - delete updatedState[lineIndex]; - this.lineCopyState = updatedState; - this.requestUpdate(); // UI 업데이트 요청 - }, 1500); - } catch (err) { - console.error('Failed to copy line:', err); - } + }).catch(err => console.error('AskView adjustWindowHeight error:', err)); } - async handleSendText() { - const textInput = this.shadowRoot?.getElementById('textInput'); - if (!textInput) return; - const text = textInput.value.trim(); - if (!text) return; - - textInput.value = ''; - - this.currentQuestion = text; - this.lineCopyState = {}; - this.showTextInput = false; - this.isLoading = true; - this.isStreaming = false; - this.currentResponse = ''; - this.accumulatedResponse = ''; - this.startHeaderAnimation(); - this.requestUpdate(); - this.renderContent(); - + requestWindowResize(targetHeight) { if (window.require) { const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('ask:sendMessage', text).catch(error => { - console.error('Error sending text:', error); - this.isLoading = false; - this.isStreaming = false; - this.currentResponse = `Error: ${error.message}`; - this.renderContent(); - }); + ipcRenderer.invoke('adjust-window-height', targetHeight); } } - handleTextKeydown(e) { - // Fix for IME composition issue: Ignore Enter key presses while composing. - if (e.isComposing) { - return; - } - - const isPlainEnter = e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey; - const isModifierEnter = e.key === 'Enter' && (e.metaKey || e.ctrlKey); - - if (isPlainEnter || isModifierEnter) { - e.preventDefault(); - this.handleSendText(); + firstUpdated() { + setTimeout(() => this.adjustWindowHeight(), 200); + + // Add scroll event listener to conversation container + const container = this.shadowRoot.getElementById('conversationContainer'); + if (container) { + container.addEventListener('scroll', () => { + this.updateScrollButton(); + }); } + + // Initial markdown rendering + this.renderMarkdownContent(); } updated(changedProperties) { super.updated(changedProperties); - if (changedProperties.has('isLoading')) { - this.renderContent(); - } - - if (changedProperties.has('showTextInput') || changedProperties.has('isLoading')) { - this.adjustWindowHeightThrottled(); - } - - if (changedProperties.has('showTextInput') && this.showTextInput) { - this.focusTextInput(); + + // Render markdown content after DOM updates + if (changedProperties.has('conversationHistory') || changedProperties.has('currentStreamingMessage')) { + this.renderMarkdownContent(); } } - focusTextInput(){ - requestAnimationFrame(() => { - const textInput = this.shadowRoot?.getElementById('textInput'); - if (textInput){ - textInput.focus(); - } - }); - } - - firstUpdated() { - setTimeout(() => this.adjustWindowHeight(), 200); - } - - handleGlobalSendRequest() { - const textInput = this.shadowRoot?.getElementById('textInput'); - if (!textInput) return; - - textInput.focus(); - - if (!textInput.value.trim()) return; - - this.handleSendText(); - } - - getTruncatedQuestion(question, maxLength = 30) { - if (!question) return ''; - if (question.length <= maxLength) return question; - return question.substring(0, maxLength) + '...'; - } - - handleInputFocus() { - this.isInputFocused = true; - } - - handleInputBlur(e) { - this.isInputFocused = false; - - // 잠시 후 포커스가 다른 곳으로 갔는지 확인 - setTimeout(() => { - const activeElement = this.shadowRoot?.activeElement || document.activeElement; - const textInput = this.shadowRoot?.getElementById('textInput'); - - // 포커스가 AskView 내부가 아니고, 응답이 없는 경우 - if (!this.currentResponse && !this.isLoading && !this.isStreaming && activeElement !== textInput && !this.isInputFocused) { - this.closeIfNoContent(); - } - }, 200); + updateScrollButton() { + const shouldShow = !this.isUserNearBottom() && this.conversationHistory.length > 0; + if (this.showScrollButton !== shouldShow) { + this.showScrollButton = shouldShow; + this.requestUpdate(); + } } render() { - const hasResponse = this.isLoading || this.currentResponse || this.isStreaming; - return html`
    - -
    + +
    -
    +
    - - +
    - ${this.headerText} -
    -
    - ${this.getTruncatedQuestion(this.currentQuestion)} -
    - - -
    + ${this.headerText}
    - -
    - + +
    + ${this.conversationHistory.length === 0 && !this.isLoading && !this.isStreaming + ? html` +
    + + + + ${this.getEmptyStateContent()} +
    + ` + : html` + ${this.conversationHistory.map((msg, index) => html` +
    +
    + ${msg.content} +
    +
    + `)} + + ${this.isLoading ? html` +
    + AI is thinking +
    +
    +
    +
    +
    +
    + ` : ''} + + ${this.isStreaming && this.currentStreamingMessage ? html` +
    +
    + ${this.currentStreamingMessage} +
    +
    + ` : ''} + ` + } + + + ${this.showScrollButton ? html` +
    + + + +
    + ` : ''}
    - -
    - +
    + +
    `; } - - // Dynamically resize the BrowserWindow to fit current content - adjustWindowHeight() { - if (!window.require) return; - - this.updateComplete.then(() => { - const headerEl = this.shadowRoot.querySelector('.response-header'); - const responseEl = this.shadowRoot.querySelector('.response-container'); - const inputEl = this.shadowRoot.querySelector('.text-input-container'); - - if (!headerEl || !responseEl) return; - - const headerHeight = headerEl.classList.contains('hidden') ? 0 : headerEl.offsetHeight; - const responseHeight = responseEl.scrollHeight; - const inputHeight = (inputEl && !inputEl.classList.contains('hidden')) ? inputEl.offsetHeight : 0; - - const idealHeight = headerHeight + responseHeight + inputHeight; - - const targetHeight = Math.min(700, idealHeight); - - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('adjust-window-height', targetHeight); - - }).catch(err => console.error('AskView adjustWindowHeight error:', err)); - } - - // Throttled wrapper to avoid excessive IPC spam (executes at most once per animation frame) - adjustWindowHeightThrottled() { - if (this.isThrottled) return; - - this.adjustWindowHeight(); - this.isThrottled = true; - - this.adjustHeightThrottle = setTimeout(() => { - this.isThrottled = false; - }, 16); - } } -customElements.define('ask-view', AskView); +customElements.define('ask-view', AskView); \ No newline at end of file diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 03e4ed0b..d554dd51 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -6,6 +6,15 @@ const sessionRepository = require('../../common/repositories/session'); const askRepository = require('./repositories'); const { getSystemPrompt } = require('../../common/prompts/promptBuilder'); +// Conversation state management +let conversationState = { + phase: 'greeting', // 'greeting', 'planning', 'task_assigned', 'feedback', 'approved' + currentTask: null, + projectDescription: null, + completedTasks: [], + currentTaskNumber: 0 +}; + function formatConversationForPrompt(conversationTexts) { if (!conversationTexts || conversationTexts.length === 0) return 'No conversation history available.'; return conversationTexts.slice(-30).join('\n'); @@ -17,6 +26,312 @@ function getConversationHistory() { return listenService ? listenService.getConversationHistory() : []; } +function resetConversationState() { + conversationState = { + phase: 'greeting', + currentTask: null, + projectDescription: null, + completedTasks: [], + currentTaskNumber: 0 + }; + + // Send initial task update + sendTaskUpdate(); +} + +function updateConversationState(newState) { + const previousPhase = conversationState.phase; + conversationState = { ...conversationState, ...newState }; + console.log(`[AskService] State updated from ${previousPhase} to ${conversationState.phase}:`, conversationState); + + // Send task updates to the task window + sendTaskUpdate(); + + // Show task window when we transition to task_assigned phase + if (conversationState.phase === 'task_assigned' && previousPhase !== 'task_assigned') { + console.log('[AskService] Transitioning to task_assigned phase, showing task window'); + showTaskWindow(); + } +} + +function sendTaskUpdate() { + console.log('[AskService] sendTaskUpdate called with state:', conversationState); + const taskWindow = windowPool.get('task'); + console.log('[AskService] taskWindow exists in sendTaskUpdate:', !!taskWindow); + + if (taskWindow && !taskWindow.isDestroyed()) { + console.log('[AskService] Sending task-updated to task window'); + taskWindow.webContents.send('task-updated', conversationState); + } else { + console.log('[AskService] Cannot send task update - window not available'); + } +} + +function showTaskWindow() { + console.log('[AskService] showTaskWindow called'); + const taskWindow = windowPool.get('task'); + console.log('[AskService] taskWindow exists:', !!taskWindow); + console.log('[AskService] Available windows:', Array.from(windowPool.keys())); + + if (taskWindow && !taskWindow.isDestroyed()) { + console.log('[AskService] taskWindow is not destroyed'); + console.log('[AskService] taskWindow.isVisible():', taskWindow.isVisible()); + + if (!taskWindow.isVisible()) { + console.log('[AskService] Showing task window'); + + // Position the window in top-right before showing + positionTaskWindowTopRight(taskWindow); + + taskWindow.show(); + taskWindow.webContents.send('window-show-animation'); + + // Update layout after showing task window + setTimeout(() => { + console.log('[AskService] Updating layout after showing task window'); + // Send IPC message to trigger layout update + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('update-layout'); + } + }); + }, 100); + } else { + console.log('[AskService] Task window is already visible'); + // Even if visible, make sure it's in the right position + positionTaskWindowTopRight(taskWindow); + } + } else { + console.log('[AskService] Task window not found or destroyed'); + console.log('[AskService] Attempting to create task window...'); + + // Try to trigger task window creation + setTimeout(() => { + const retryTaskWindow = windowPool.get('task'); + if (retryTaskWindow) { + console.log('[AskService] Task window found after retry'); + positionTaskWindowTopRight(retryTaskWindow); + retryTaskWindow.show(); + retryTaskWindow.webContents.send('window-show-animation'); + } else { + console.log('[AskService] Task window still not available after retry'); + } + }, 500); + } +} + +function positionTaskWindowTopRight(taskWindow) { + if (!taskWindow || taskWindow.isDestroyed()) return; + + const { screen } = require('electron'); + const primaryDisplay = screen.getPrimaryDisplay(); + const { x: workAreaX, y: workAreaY, width: screenWidth, height: screenHeight } = primaryDisplay.workArea; + + const taskBounds = taskWindow.getBounds(); + const PAD = 16; + + // Position in top right corner + const taskX = workAreaX + screenWidth - taskBounds.width - PAD; + const taskY = workAreaY + PAD; + + console.log('[AskService] Positioning task window at top-right:', { taskX, taskY, screenWidth, screenHeight }); + + taskWindow.setBounds({ + x: taskX, + y: taskY, + width: taskBounds.width, + height: taskBounds.height + }); +} + +function buildContextualPrompt(userPrompt, conversationHistory) { + let contextualPrompt = `Current conversation phase: ${conversationState.phase}\n`; + + if (conversationState.projectDescription) { + contextualPrompt += `Project description: ${conversationState.projectDescription}\n`; + } + + if (conversationState.completedTasks.length > 0) { + contextualPrompt += `Completed tasks: ${conversationState.completedTasks.join(', ')}\n`; + } + + if (conversationState.currentTask) { + contextualPrompt += `Current task: ${conversationState.currentTask}\n`; + } + + contextualPrompt += `User message: ${userPrompt}\n`; + contextualPrompt += `Conversation history: ${conversationHistory}\n`; + + return contextualPrompt; +} + +function processStateTransition(userPrompt, aiResponse) { + console.log(`[AskService] Processing state transition from: ${conversationState.phase}`); + console.log(`[AskService] User prompt: ${userPrompt}`); + console.log(`[AskService] AI Response: ${aiResponse}`); + + switch (conversationState.phase) { + case 'greeting': + // User responds to "What are you planning to design?" + console.log('[AskService] Greeting -> Planning transition'); + updateConversationState({ + phase: 'planning', + projectDescription: userPrompt.trim() + }); + break; + + case 'planning': + // This happens when AI responds with a task assignment + console.log('[AskService] Planning -> Task_assigned transition'); + const firstTask = extractTaskFromResponse(aiResponse); + console.log('[AskService] Planning phase: extracted task =', firstTask); + updateConversationState({ + phase: 'task_assigned', + currentTask: firstTask, + currentTaskNumber: 1 + }); + break; + + case 'task_assigned': + // User is working on task, AI gives feedback + console.log('[AskService] Task_assigned -> Feedback transition'); + updateConversationState({ + phase: 'feedback' + }); + break; + + case 'feedback': + // Check if AI approved the task + if (isTaskApproved(aiResponse)) { + console.log('[AskService] Task approved, moving to next task'); + const nextTask = extractTaskFromResponse(aiResponse); + updateConversationState({ + phase: 'task_assigned', + completedTasks: [...conversationState.completedTasks, conversationState.currentTask], + currentTask: nextTask, + currentTaskNumber: conversationState.currentTaskNumber + 1 + }); + } else { + console.log('[AskService] Task needs improvement, staying in feedback phase'); + // Still giving feedback, stay in feedback phase but update current task if needed + const revisedTask = extractTaskFromResponse(aiResponse); + updateConversationState({ + phase: 'task_assigned', // Go back to task assigned to let user try again + currentTask: revisedTask || conversationState.currentTask + }); + } + break; + + case 'approved': + // Should not stay here long, transitions to task_assigned + break; + } +} + +function extractTaskFromResponse(response) { + console.log('[AskService] Extracting task from response:', response); + + // Clean up markdown formatting for better extraction + const cleanResponse = response.replace(/\*\*/g, ''); // Remove ** markdown + + // Enhanced extraction - look for task-like patterns + const taskPatterns = [ + // "Task 1: Do something" + /(?:Task|Step)\s+\d+:?\s*(.+?)(?:\.|$)/i, + // "Let's start by doing X" - capture everything until period + /(?:Let's start by|Let's begin by|Start by|Begin by)\s+(.+?)(?:\.|$)/i, + // "Try changing/doing X" - capture complete instruction + /(?:Try|Please try)\s+(.+?)(?:\.|$)/i, + // "First, do X" or "Next, do X" - capture full instruction + /(?:First,?|Next,?|Now,?)\s+(.+?)(?:\.|$)/i, + // "Create X" or "Add X" or "Design X" - capture full instruction + /(?:Create|Add|Design|Build|Make|Set up|Define|Adjust|Change)\s+(.+?)(?:\.|$)/i, + // Direct action patterns + /(?:You should|Go ahead and)\s+(.+?)(?:\.|$)/i + ]; + + for (const pattern of taskPatterns) { + const match = cleanResponse.match(pattern); + if (match) { + let task = match[1].trim(); + // If the extracted task is too short, try to get more context + if (task.length < 20) { + // Look for the sentence containing this pattern and extract the full sentence + const sentences = cleanResponse.split(/[.!?]+/); + for (const sentence of sentences) { + if (sentence.toLowerCase().includes(task.toLowerCase().substring(0, 10))) { + task = sentence.trim(); + break; + } + } + } + console.log('[AskService] Extracted task:', task); + return task; + } + } + + // If no specific pattern found, look for imperative sentences + const sentences = cleanResponse.split(/[.!?]+/); + for (const sentence of sentences) { + const trimmed = sentence.trim(); + // Look for sentences that start with action verbs + if (trimmed.match(/^(Create|Add|Design|Build|Make|Set|Define|Write|Draw|Place|Position|Layout|Structure|Try|Change|Adjust)/i)) { + console.log('[AskService] Found action sentence:', trimmed); + return trimmed; + } + } + + // Enhanced fallback: look for sentences with "let's" or task-like content + for (const sentence of sentences) { + const trimmed = sentence.trim(); + if (trimmed.toLowerCase().includes('let\'s') && trimmed.length > 15) { + console.log('[AskService] Found let\'s sentence:', trimmed); + return trimmed; + } + } + + // Final fallback: return first meaningful sentence + const fallback = sentences.find(s => s.trim().length > 10)?.trim() || 'Continue working on your design'; + console.log('[AskService] Using fallback task:', fallback); + return fallback; +} + +function isTaskApproved(response) { + const lowerResponse = response.toLowerCase(); + + // Strong approval indicators + const approvalWords = [ + 'excellent', 'perfect', 'great job', 'well done', 'nice work', + 'looks good', 'that works', 'approved', 'correct' + ]; + + // Next task indicators (strong sign of approval) + const nextTaskIndicators = [ + 'now let\'s', 'next', 'now we can', 'now add', 'now create', + 'now design', 'now set up', 'now try', 'move on to' + ]; + + // Check for strong approval + const hasApproval = approvalWords.some(word => lowerResponse.includes(word)); + + // Check for next task assignment (indicates previous task was approved) + const hasNextTask = nextTaskIndicators.some(phrase => lowerResponse.includes(phrase)); + + // Look for task numbering increase (Task 2, Step 2, etc.) + const hasTaskProgression = /(?:task|step)\s+[2-9]/i.test(response); + + const isApproved = hasApproval || hasNextTask || hasTaskProgression; + + console.log('[AskService] Task approval check:', { + hasApproval, + hasNextTask, + hasTaskProgression, + isApproved + }); + + return isApproved; +} + async function sendMessage(userPrompt) { if (!userPrompt || userPrompt.trim().length === 0) { console.warn('[AskService] Cannot process empty message'); @@ -30,6 +345,7 @@ async function sendMessage(userPrompt) { try { console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`); + console.log(`[AskService] Current state: ${conversationState.phase}`); const screenshotResult = await captureScreenshot({ quality: 'medium' }); const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null; @@ -37,7 +353,9 @@ async function sendMessage(userPrompt) { const conversationHistoryRaw = getConversationHistory(); const conversationHistory = formatConversationForPrompt(conversationHistoryRaw); - const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false); + // Build context-aware prompt based on current state + const contextualPrompt = buildContextualPrompt(userPrompt, conversationHistory); + const systemPrompt = getSystemPrompt('figma_design_tutor', contextualPrompt, false); const API_KEY = await getStoredApiKey(); if (!API_KEY) { @@ -102,6 +420,15 @@ async function sendMessage(userPrompt) { if (data === '[DONE]') { askWin.webContents.send('ask-response-stream-end'); + // Process state transitions based on response + processStateTransition(userPrompt, fullResponse); + + // After processing user state transition, check if AI response should trigger another transition + if (conversationState.phase === 'planning') { + console.log('[AskService] AI responded in planning phase, processing task assignment'); + processStateTransition('', fullResponse); // Empty user prompt for AI response + } + // Save to DB try { const uid = authService.getCurrentUserId(); @@ -139,6 +466,25 @@ function initialize() { ipcMain.handle('ask:sendMessage', async (event, userPrompt) => { return sendMessage(userPrompt); }); + + ipcMain.handle('ask:resetConversation', async (event) => { + resetConversationState(); + return { success: true }; + }); + + ipcMain.handle('ask:getConversationState', async (event) => { + return conversationState; + }); + + // Debug handler to manually show task window + ipcMain.handle('ask:showTaskWindow', async (event) => { + showTaskWindow(); + return { success: true }; + }); + + // Send initial task update on service start + sendTaskUpdate(); + console.log('[AskService] Initialized and ready.'); } diff --git a/src/features/listen/summary/summaryService.js b/src/features/listen/summary/summaryService.js index 58a4ecc1..8a5c726f 100644 --- a/src/features/listen/summary/summaryService.js +++ b/src/features/listen/summary/summaryService.js @@ -107,7 +107,7 @@ Please build upon this context while analyzing the new conversation segments. `; } - const basePrompt = getSystemPrompt('pickle_glass_analysis', '', false); + const basePrompt = getSystemPrompt('figma_design_tutor', '', false); const systemPrompt = basePrompt.replace('{{CONVERSATION_HISTORY}}', recentConversation); try { diff --git a/src/features/task/TaskView.js b/src/features/task/TaskView.js new file mode 100644 index 00000000..8420aba5 --- /dev/null +++ b/src/features/task/TaskView.js @@ -0,0 +1,457 @@ +import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; + +export class TaskView extends LitElement { + static properties = { + currentTask: { type: String }, + taskNumber: { type: Number }, + projectDescription: { type: String }, + completedTasks: { type: Array }, + phase: { type: String }, + isVisible: { type: Boolean } + }; + + static styles = css` + :host { + display: block; + width: 100%; + height: 100%; + color: white; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out; + will-change: transform, opacity; + } + + :host(.hiding) { + animation: slideOut 0.3s cubic-bezier(0.4, 0, 0.6, 1) forwards; + } + + :host(.showing) { + animation: slideIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + } + + :host(.hidden) { + opacity: 0; + transform: translateX(100%) scale(0.85); + pointer-events: none; + } + + @keyframes slideOut { + 0% { + opacity: 1; + transform: translateX(0) scale(1); + filter: blur(0px); + } + 100% { + opacity: 0; + transform: translateX(100%) scale(0.85); + filter: blur(2px); + } + } + + @keyframes slideIn { + 0% { + opacity: 0; + transform: translateX(100%) scale(0.85); + filter: blur(2px); + } + 100% { + opacity: 1; + transform: translateX(0) scale(1); + filter: blur(0px); + } + } + + * { + font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + cursor: default; + user-select: none; + } + + .task-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + background: rgba(0, 0, 0, 0.65); + border-radius: 12px; + outline: 0.5px rgba(255, 255, 255, 0.3) solid; + outline-offset: -1px; + backdrop-filter: blur(10px); + box-sizing: border-box; + position: relative; + overflow: hidden; + min-width: 280px; + max-width: 400px; + } + + .task-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border-radius: 12px; + filter: blur(10px); + z-index: -1; + } + + .task-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: transparent; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; + } + + .header-left { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + .task-icon { + width: 20px; + height: 20px; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .task-icon svg { + width: 12px; + height: 12px; + stroke: rgba(255, 255, 255, 0.9); + } + + .task-title { + font-size: 13px; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + white-space: nowrap; + } + + .task-content { + flex: 1; + padding: 16px; + overflow-y: auto; + overflow-x: hidden; + background: transparent; + min-height: 0; + display: flex; + flex-direction: column; + gap: 12px; + } + + .task-content::-webkit-scrollbar { + width: 6px; + } + + .task-content::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; + margin: 4px; + } + + .task-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + + .task-content::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.35); + } + + .current-task { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; + } + + .task-number { + font-size: 11px; + color: rgba(255, 255, 255, 0.6); + font-weight: 500; + margin-bottom: 4px; + } + + .task-description { + font-size: 13px; + line-height: 1.4; + color: rgba(255, 255, 255, 0.9); + word-wrap: break-word; + } + + .project-info { + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 8px; + } + + .project-name { + font-weight: 500; + color: rgba(255, 255, 255, 0.85); + } + + .completed-tasks { + margin-top: 8px; + } + + .completed-title { + font-size: 11px; + color: rgba(255, 255, 255, 0.6); + font-weight: 500; + margin-bottom: 8px; + } + + .completed-task { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.2); + border-radius: 6px; + padding: 8px; + margin-bottom: 6px; + font-size: 12px; + color: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + gap: 6px; + } + + .completed-task::before { + content: '✓'; + color: rgba(34, 197, 94, 0.8); + font-weight: bold; + font-size: 12px; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 40px 20px; + text-align: center; + color: rgba(255, 255, 255, 0.7); + min-height: 200px; + } + + .empty-state-icon { + width: 32px; + height: 32px; + stroke: rgba(255, 255, 255, 0.5); + stroke-width: 1.5; + } + + .greeting-state { + padding: 20px 16px; + text-align: center; + color: rgba(255, 255, 255, 0.8); + } + + .greeting-text { + font-size: 14px; + margin-bottom: 8px; + } + + .greeting-subtitle { + font-size: 12px; + color: rgba(255, 255, 255, 0.6); + } + + /* ────────────────[ GLASS BYPASS ]─────────────── */ + :host-context(body.has-glass) .task-container { + background: transparent !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + } + + :host-context(body.has-glass) .task-container::before { + display: none !important; + } + + :host-context(body.has-glass) .current-task, + :host-context(body.has-glass) .completed-task { + background: transparent !important; + border: none !important; + } + + :host-context(body.has-glass) .task-content::-webkit-scrollbar-track, + :host-context(body.has-glass) .task-content::-webkit-scrollbar-thumb { + background: transparent !important; + } + `; + + constructor() { + super(); + this.currentTask = ''; + this.taskNumber = 0; + this.projectDescription = ''; + this.completedTasks = []; + this.phase = 'greeting'; + this.isVisible = false; + } + + connectedCallback() { + super.connectedCallback(); + console.log('📋 TaskView connected'); + + if (window.require) { + const { ipcRenderer } = window.require('electron'); + + // Listen for task updates from the ask service + ipcRenderer.on('task-updated', (event, taskData) => { + this.updateTask(taskData); + }); + + // Listen for window show/hide events + ipcRenderer.on('window-show-animation', () => { + this.classList.add('showing'); + this.classList.remove('hiding', 'hidden'); + }); + + ipcRenderer.on('window-hide-animation', () => { + this.classList.add('hiding'); + this.classList.remove('showing'); + }); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.removeAllListeners('task-updated'); + ipcRenderer.removeAllListeners('window-show-animation'); + ipcRenderer.removeAllListeners('window-hide-animation'); + } + } + + updateTask(taskData) { + this.currentTask = taskData.currentTask || ''; + this.taskNumber = taskData.currentTaskNumber || 0; + this.projectDescription = taskData.projectDescription || ''; + this.completedTasks = taskData.completedTasks || []; + this.phase = taskData.phase || 'greeting'; + this.requestUpdate(); + } + + getHeaderText() { + switch (this.phase) { + case 'greeting': + return 'Getting Started'; + case 'planning': + return 'Planning Project'; + case 'task_assigned': + return `Task ${this.taskNumber}`; + case 'feedback': + return 'Review & Feedback'; + case 'approved': + return 'Task Complete!'; + default: + return 'Current Task'; + } + } + + renderContent() { + if (this.phase === 'greeting') { + return html` +
    +
    Ready to start designing!
    +
    Tell the tutor what you'd like to create
    +
    + `; + } + + if (this.phase === 'planning') { + return html` +
    +
    + Project: ${this.projectDescription} +
    +
    +
    Planning your design...
    +
    The tutor will assign your first task
    +
    +
    + `; + } + + if (!this.currentTask && this.completedTasks.length === 0) { + return html` +
    + + + + No active task + Start a conversation to begin +
    + `; + } + + return html` +
    + ${this.projectDescription ? html` +
    + Project: ${this.projectDescription} +
    + ` : ''} + + ${this.currentTask ? html` +
    +
    Task ${this.taskNumber}
    +
    ${this.currentTask}
    +
    + ` : ''} + + ${this.completedTasks.length > 0 ? html` +
    +
    Completed (${this.completedTasks.length})
    + ${this.completedTasks.map(task => html` +
    ${task}
    + `)} +
    + ` : ''} +
    + `; + } + + render() { + return html` +
    + +
    +
    +
    + + + +
    + ${this.getHeaderText()} +
    +
    + + + ${this.renderContent()} +
    + `; + } +} + +customElements.define('task-view', TaskView); \ No newline at end of file