Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Technician - Search">
    <!-- Toolbar at top of screen -->
    <Toolbar @help-clicked="openInfoModal" />

    <div class="find-job-container">
      <!-- New Search Bar Component -->
      <div class="new-search-bar-section">
        <h2 class="new-search-title">What are we doing today?</h2>
        <div class="new-search-tags-container">
          <!-- Search term chips (without #) -->
          <ion-chip
            v-for="(term, index) in appliedSearchTerms"
            :key="'new-search-term-' + index"
            color="secondary"
            class="new-search-tag-chip new-search-term-chip"
            @click="removeSearchTerm(index)"
          >
            {{ term }}
          </ion-chip>
          <!-- Selected tag chips (without #) in alternate color -->
          <template v-for="tagId in selectedTags" :key="'selected-tag-' + tagId">
            <ion-chip
              v-if="getTagNameById(tagId)"
              color="tertiary"
              class="new-search-tag-chip new-search-term-chip"
              @click="removeSelectedTag(tagId)"
            >
              {{ getTagNameById(tagId) }}
            </ion-chip>
          </template>
          <!-- Available tag chips (with #) -->
          <ion-chip
            v-for="tag in topTagsForNewSearch"
            :key="`new-search-${tag.id}`"
            @click="selectTag(tag.id)"
            color="primary"
            class="new-search-tag-chip"
          >
            {{ "#" + tag.name }}
          </ion-chip>
        </div>
        <div class="new-search-bar">
          <div class="new-search-input-container">
            <ion-input
              v-model="searchQuery"
              placeholder="Search jobs..."
              fill="outline"
              class="new-search-input"
              @keydown="handleSearchKeydown"
              @input="handleSearchInput"
              @ionFocus="handleSearchFocus"
              @ionBlur="handleSearchBlur"
            ></ion-input>
            <!-- Suggestions dropdown -->
            <div
              v-if="showSuggestions && searchSuggestions.length > 0"
              class="suggestions-dropdown"
            >
              <ion-list>
                <ion-item
                  v-for="(suggestion, index) in searchSuggestions"
                  :key="`${suggestion.type}-${suggestion.id}`"
                  button
                  @click="selectSuggestion(suggestion)"
                  :class="{
                    'suggestion-selected': index === selectedSuggestionIndex,
                  }"
                  class="suggestion-item"
                >
                  <ion-icon
                    :icon="suggestion.type === 'tag' ? pricetag : briefcase"
                    slot="start"
                    :class="suggestion.type === 'tag' ? 'tag-icon' : 'job-icon'"
                  ></ion-icon>
                  <ion-label>{{ suggestion.name }}</ion-label>
                </ion-item>
              </ion-list>
            </div>
          </div>
          <ion-button fill="solid" color="secondary" @click="applySearch">
            Search
          </ion-button>
          <ion-button fill="outline" color="tertiary" @click="clearSearch">
            Clear
          </ion-button>
          <div v-if="canUndo" class="new-search-undo-wrapper">
            <ion-button
              @click="undoLastAction"
              fill="solid"
              color="warning"
              class="new-search-undo-button new-search-undo-shadow"
              size="default"
            >
              <ion-icon :icon="arrowUndo" slot="icon-only"></ion-icon>
            </ion-button>
            <ion-button
              @click="undoLastAction"
              fill="solid"
              color="medium"
              class="new-search-undo-button new-search-undo-front"
              size="default"
            >
              <ion-icon
                :icon="arrowUndo"
                slot="icon-only"
                color="tertiary"
              ></ion-icon>
            </ion-button>
          </div>
        </div>
      </div>

      <!-- Available jobs list (Google search result style) -->
      <div
        v-if="selectedTags.length > 0 || appliedSearchQuery.trim() !== ''"
        class="available-jobs-section"
      >
        <div v-if="filteredProblems.length === 0" class="no-jobs-message">
          <p>
            No jobs match your selected filters. Try removing some tags or
            changing your search.
          </p>
        </div>
        <div v-else class="search-results">
          <div
            v-for="problem in filteredProblems"
            :key="problem.id"
            @click="selectJob(problem)"
            class="search-result-item"
          >
            <div class="result-content">
              <h3 class="result-title">{{ problem.name }}</h3>
              <p
                class="result-description"
                v-html="formatDescriptionWithHashtags(problem.description)"
              ></p>
            </div>
            <ion-button
              fill="solid"
              color="tertiary"
              size="small"
              class="details-button"
              @click.stop="selectJob(problem)"
            >
              View
            </ion-button>
          </div>
        </div>
      </div>
    </div>

    <!-- Action Bar -->
    <div class="action-bar action-bar-disabled">
      <div class="action-bar-content">
        <h2 class="action-bar-text">Add Job</h2>
      </div>
    </div>

    <!-- Info Modal -->
    <ion-modal :is-open="isInfoModalOpen" @didDismiss="closeInfoModal">
      <ion-header>
        <ion-toolbar>
          <ion-title>{{
            showDebugInfo ? "Tag Information Gain Data" : "How to Find Your Job"
          }}</ion-title>
          <ion-buttons slot="start">
            <ion-button @click="toggleDebugInfo">
              <ion-icon :icon="helpCircleOutline"></ion-icon>
            </ion-button>
          </ion-buttons>
          <ion-buttons slot="end">
            <ion-button @click="closeInfoModal">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <!-- Debug Info View -->
        <div v-if="showDebugInfo" class="debug-content">
          <div class="debug-summary">
            <p>
              <strong>Total Filtered Problems:</strong>
              {{ debugInfoData.totalProblems }}
            </p>
            <p>
              <strong>Current Entropy:</strong>
              {{ debugInfoData.currentEntropy.toFixed(3) }}
            </p>
            <p>
              <strong>Showing Top {{ debugInfoData.tags.length }} Tags</strong>
            </p>
          </div>

          <div class="debug-tags-list">
            <div
              v-for="tag in debugInfoData.tags"
              :key="tag.tagId"
              class="debug-tag-item"
            >
              <div class="debug-tag-rank">{{ tag.rank }}</div>
              <div class="debug-tag-details">
                <div class="debug-tag-name">{{ tag.tagName }}</div>
                <div class="debug-tag-stats">
                  <span>Gain: {{ tag.informationGain.toFixed(3) }}</span>
                  <span class="debug-divider">|</span>
                  <span
                    >Appears in {{ tag.problemCount }}/{{
                      debugInfoData.totalProblems
                    }}
                    jobs</span
                  >
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- Info Content View -->
        <div v-else class="info-content">
          <h3>How to Find Your Job</h3>

          <h4>Search by Text</h4>
          <p>
            Type keywords into the search bar to find jobs. As you type, you'll
            see suggestions for matching tags and job titles. Press Enter or
            click the Search button to apply your search. Your search terms will
            appear as blue chips in the tags area.
          </p>

          <h4>Filter by Tags</h4>
          <p>
            Click on any tag (marked with #) to filter the job list. Only jobs
            matching your selected tags will be shown. Tags turn green when
            selected. The tag list automatically updates to show only relevant
            tags based on your current selection.
          </p>

          <h4>Browse Results</h4>
          <p>
            Matching jobs appear below the tags as a list. Click any job title
            to preview details, then select "Select Job" to add it to your
            session.
          </p>

          <h4>Refine Your Search</h4>
          <p>
            Click the yellow Undo button to step back through your selections.
            Click any search term or tag chip to remove it. Use the Clear button
            to start over.
          </p>

          <hr
            style="
              margin: 24px 0;
              border: 1px solid var(--ion-color-medium);
              opacity: 0.3;
            "
          />

          <h4>How Tags Work (Advanced)</h4>
          <p>
            Job tags are organized by "information gain" (each tag's information
            gain is a measurement of how great its impact is on filtering down
            the jobs). Jobs with the highest information gain are shown with
            bold text and are mixed into the tag display more frequently. Tags
            are also sorted by information gain, so all of the highest
            information gain tags appear first.
          </p>
          <p>
            When a tag is selected, all of the jobs that do not match that tag
            are removed from the pool of jobs, and the pool of tags is refreshed
            to only include tags that appear in the filtered-down pool of jobs.
            With this behavior, you should be able to quickly find your job.
          </p>
          <p>
            You can also find job by searching via text. Entered text will match
            job titles, descriptions and tags. As you enter text,
            auto-suggestions are made available based on tags and job titles.
          </p>

          <hr
            style="
              margin: 24px 0;
              border: 1px solid var(--ion-color-medium);
              opacity: 0.3;
            "
          />

          <h4>Export Jobs</h4>
          <p>
            Export the currently filtered jobs (respecting your search terms and
            selected tags) to CSV or JSON format.
          </p>

          <!-- CSV Preview -->
          <div class="csv-preview-container">
            <pre class="csv-preview">{{ csvPreviewData }}</pre>
          </div>

          <div class="export-buttons">
            <ion-button
              expand="block"
              color="secondary"
              @click="copyCsvToClipboard"
            >
              Copy CSV to Clipboard
            </ion-button>
            <ion-button
              expand="block"
              color="primary"
              @click="handleExportJobs"
            >
              Export JSON
            </ion-button>
          </div>
        </div>
      </ion-content>
    </ion-modal>

    <!-- Bookmarks Modal -->
    <ion-modal
      :is-open="isBookmarksModalOpen"
      @didDismiss="closeBookmarksModal"
    >
      <ion-header>
        <ion-toolbar>
          <ion-title>Bookmarks</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="closeBookmarksModal">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="bookmarks-content">
          <div class="bookmarks-list">
            <div
              v-for="job in bookmarkedJobs"
              :key="job.id"
              @click="selectBookmarkedJob(job)"
              class="bookmark-item"
            >
              <ion-icon
                :icon="star"
                color="warning"
                class="bookmark-star"
              ></ion-icon>
              <div class="bookmark-details">
                <h3 class="bookmark-title">{{ job.name }}</h3>
                <p
                  class="bookmark-description"
                  v-html="formatDescriptionWithHashtags(job.description)"
                ></p>
              </div>
            </div>
          </div>
        </div>
      </ion-content>
    </ion-modal>
  </BaseLayout>
</template>

<script setup lang="ts">

/*
          <ion-chip
            v-for="tagItem in topTagsForNewSearch"
            :key="`new-search-${tagItem.tag?.id}`"
            @click="selectTagProblems(tagItem)"
            :color="getChipColor(tagItem)"
            class="new-search-tag-chip"
            v-if="'false'" <-- tagItem.tag?.id && tagItem.tag?.name" --/>
          >
            {{ "#" // + tagItem.tag?.name }}
          </ion-chip>

*/

import { ref, computed, onMounted, watch } from "vue";
import BaseLayout from "@/components/BaseLayout.vue";
import Toolbar from "@/components/Toolbar.vue";
import type { ProblemsRecord, ProblemTagsRecord } from "@/pocketbase-types";
import {
  IonInput,
  IonButton,
  IonChip,
  IonIcon,
  IonModal,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonButtons,
  IonContent,
  IonList,
  IonItem,
  IonLabel,
} from "@ionic/vue";
import {
  arrowBack,
  star,
  pricetag,
  briefcase,
  helpCircleOutline,
  arrowUndo,
} from "ionicons/icons";
import { useRouter, useRoute } from "vue-router";
import { useSessionStore } from "@/stores/session";

const router = useRouter();
const route = useRoute();
const sessionStore = useSessionStore();

const goToDashboard = () => {
  router.push("/service-call");
};

// State
const searchQuery = ref("");
const isInfoModalOpen = ref(false);
const showDebugInfo = ref(false);
const isBookmarksModalOpen = ref(false);
const csvPreviewData = ref("");
const isLoadingCsv = ref(false);
const isSyncingFromUrl = ref(false);

// Get data from session store
const problems = computed(() => sessionStore.problems);
const problemTags = computed(() => sessionStore.problemTags);
const selectedTags = computed(() => sessionStore.selectedTags);
const appliedSearchQuery = computed(() => sessionStore.appliedSearchQuery);
const selectionHistory = computed(() => sessionStore.selectionHistory);

// Auto-suggestions state
const showSuggestions = ref(false);
const selectedSuggestionIndex = ref(-1);

// Helper function to get tag IDs from a problem (use store method)
const getProblemTagIds = (problem: ProblemsRecord): string[] => {
  return sessionStore.getProblemTagIds(problem);
};

// Helper function to get tag names for a problem
const getTagsForProblem = (problem: ProblemsRecord): string[] => {
  const tagIds = getProblemTagIds(problem);

  return tagIds
    .map((tagId) => problemTags.value.find((tag) => tag.id === tagId))
    .filter((tag) => tag !== undefined)
    .map((tag) => tag!.name!)
    .filter((name) => name !== undefined);
};

// Undo last action (use store)
const undoLastAction = async () => {
  await sessionStore.undoSearch();
};

// Check if undo is available
const canUndo = computed(() => selectionHistory.value.length > 0);

// Split applied search query into individual terms for display as chips
const appliedSearchTerms = computed(() => {
  if (appliedSearchQuery.value.trim() === "") return [];
  return appliedSearchQuery.value
    .split(/\s+/)
    .filter((term) => term.trim() !== "");
});

// Remove a search term by index (use store)
const removeSearchTerm = async (index: number) => {
  await sessionStore.removeSearchTerm(index);
};

// Computed property for filtered problems based on selected tags and search
const filteredProblems = computed(() => {
  let filtered = problems.value;

  // Filter by selected tags
  if (selectedTags.value.length > 0) {
    filtered = filtered.filter((problem) => {
      const problemTagIds = getProblemTagIds(problem);
      // Job must have ALL selected tags (AND logic)
      return selectedTags.value.every((tagId) => problemTagIds.includes(tagId));
    });
  }

  // Filter by search query (use appliedSearchQuery)
  if (appliedSearchQuery.value.trim() !== "") {
    const searchTerms = appliedSearchQuery.value
      .toLowerCase()
      .split(/\s+/) // Split on any whitespace
      .filter((term) => term.trim() !== "");

    filtered = filtered.filter((problem) => {
      const problemName = problem.name?.toLowerCase() || "";
      const problemDescription = problem.description?.toLowerCase() || "";
      const searchText = `${problemName} ${problemDescription}`;

      // All search terms must be found in the combined text (AND logic)
      return searchTerms.every((term) => searchText.includes(term));
    });
  }

  console.log(
    "[filteredProblems] appliedSearchQuery:",
    appliedSearchQuery.value,
    "selectedTags:",
    selectedTags.value.length,
    "result count:",
    filtered.length,
    selectedTags
  );
  return filtered;
});

// Computed property for tags and problems sorted by frequency (only from filtered problems)
const sortedTagsByFrequency = computed(() => {
  if (problemTags.value.length === 0 || filteredProblems.value.length === 0)
    return [];

  const itemFrequency = new Map<
    string,
    {
      tag?: ProblemTagsRecord;
      problem?: ProblemsRecord;
      problems: ProblemsRecord[];
      count: number;
      type: "tag" | "problem";
    }
  >();

  // First, collect all tag IDs that exist on filtered problems
  const tagIdsInFilteredProblems = new Set<string>();
  filteredProblems.value.forEach((problem) => {
    const tagIds = getProblemTagIds(problem);
    tagIds.forEach((tagId) => tagIdsInFilteredProblems.add(tagId));
  });

  // Add actual tags (only those that exist on filtered problems, excluding selected tags)
  problemTags.value.forEach((tag) => {
    // Skip tags that are already selected
    if (selectedTags.value.includes(tag.id)) {
      return;
    }

    // Skip tags that don't exist on any filtered problems
    if (!tagIdsInFilteredProblems.has(tag.id)) {
      return;
    }

    const problemsWithTag = filteredProblems.value.filter((problem) => {
      const tagIds = getProblemTagIds(problem);
      return tagIds.includes(tag.id);
    });

    // Show all tags that exist on the filtered problems (cross-filtering)
    if (problemsWithTag.length > 0) {
      itemFrequency.set(`tag-${tag.id}`, {
        tag,
        problems: problemsWithTag,
        count: problemsWithTag.length,
        type: "tag",
      });
    }
  });

  // Add problems as "tags" (job titles)
  filteredProblems.value.forEach((problem) => {
    itemFrequency.set(`problem-${problem.id}`, {
      problem,
      problems: [problem],
      count: 1,
      type: "problem",
    });
  });

  // Calculate information gain for each tag (inline, to avoid circular dependency)
  const totalProblems = filteredProblems.value.length;
  const currentEntropy = totalProblems > 1 ? Math.log2(totalProblems) : 0;
  const infoGainMap = new Map<string, number>();

  // Calculate info gain for all tags
  Array.from(itemFrequency.values()).forEach((item) => {
    if (item.type !== "tag" || !item.tag) return;

    const tagId = item.tag.id;
    const countWith = item.count;
    const countWithout = totalProblems - countWith;

    // Calculate weighted entropy after split
    let entropyAfterSplit = 0;

    if (countWith > 0) {
      const probWith = countWith / totalProblems;
      const entropyWith = countWith > 1 ? Math.log2(countWith) : 0;
      entropyAfterSplit += probWith * entropyWith;
    }

    if (countWithout > 0) {
      const probWithout = countWithout / totalProblems;
      const entropyWithout = countWithout > 1 ? Math.log2(countWithout) : 0;
      entropyAfterSplit += probWithout * entropyWithout;
    }

    // Information gain = entropy before - weighted entropy after
    const informationGain = currentEntropy - entropyAfterSplit;
    infoGainMap.set(tagId, informationGain);
  });

  // Sort by type first (tags before problems), then by information gain (descending), then by name
  const sorted = Array.from(itemFrequency.values()).sort((a, b) => {
    // Tags come before problems
    if (a.type !== b.type) {
      return a.type === "tag" ? -1 : 1;
    }

    // For tags, sort by information gain
    if (a.type === "tag" && b.type === "tag" && a.tag && b.tag) {
      const gainA = infoGainMap.get(a.tag.id) || 0;
      const gainB = infoGainMap.get(b.tag.id) || 0;

      // Use small epsilon for float comparison
      if (Math.abs(gainA - gainB) > 0.001) {
        return gainB - gainA; // Higher info gain first
      }
    }

    // If info gain is same (or for problems), fall back to alphabetical
    const aName = a.tag?.name || a.problem?.name || "";
    const bName = b.tag?.name || b.problem?.name || "";
    return aName.localeCompare(bName);
  });

  // Debug: Log first 10 sorted tags with their info gain
  console.log("=== SORTED TAGS (First 10) ===");
  sorted
    .filter((item) => item.type === "tag")
    .slice(0, 10)
    .forEach((item, index) => {
      if (item.tag) {
        const gain = infoGainMap.get(item.tag.id) || 0;
        console.log(
          `${index + 1}. ${item.tag.name}: gain=${gain.toFixed(3)}, count=${item.count}`,
        );
      }
    });

  return sorted;
});

// Computed property to calculate information gain for each tag
const tagInformationGain = computed(() => {
  if (problemTags.value.length === 0 || filteredProblems.value.length === 0) {
    return new Map<string, number>();
  }

  const totalProblems = filteredProblems.value.length;
  const informationGainMap = new Map<string, number>();

  // Calculate entropy before adding any tag (current state)
  const currentEntropy = totalProblems > 1 ? Math.log2(totalProblems) : 0;

  // For each available tag, calculate information gain
  sortedTagsByFrequency.value.forEach((item) => {
    if (item.type !== "tag" || !item.tag) return;

    const tagId = item.tag.id;

    // Count problems with and without this tag
    const problemsWithTag = filteredProblems.value.filter((problem) => {
      const tagIds = getProblemTagIds(problem);
      return tagIds.includes(tagId);
    });
    const problemsWithoutTag = filteredProblems.value.filter((problem) => {
      const tagIds = getProblemTagIds(problem);
      return !tagIds.includes(tagId);
    });

    const countWith = problemsWithTag.length;
    const countWithout = problemsWithoutTag.length;

    // Calculate weighted entropy after split
    let entropyAfterSplit = 0;

    if (countWith > 0) {
      const probWith = countWith / totalProblems;
      const entropyWith = countWith > 1 ? Math.log2(countWith) : 0;
      entropyAfterSplit += probWith * entropyWith;
    }

    if (countWithout > 0) {
      const probWithout = countWithout / totalProblems;
      const entropyWithout = countWithout > 1 ? Math.log2(countWithout) : 0;
      entropyAfterSplit += probWithout * entropyWithout;
    }

    // Information gain = entropy before - weighted entropy after
    const informationGain = currentEntropy - entropyAfterSplit;

    informationGainMap.set(tagId, informationGain);
  });

  // Log the information gain for all tags
  console.log("=== INFORMATION GAIN ANALYSIS ===");
  console.log("Total filtered problems:", totalProblems);
  console.log("Current entropy:", currentEntropy.toFixed(3));

  const gainArray = Array.from(informationGainMap.entries())
    .map(([tagId, gain]) => ({
      tagId,
      tagName: getTagNameById(tagId),
      informationGain: gain,
      problemCount:
        sortedTagsByFrequency.value.find(
          (item) => item.type === "tag" && item.tag?.id === tagId,
        )?.count || 0,
    }))
    .sort((a, b) => b.informationGain - a.informationGain);

  console.log("Tags ranked by information gain:");
  gainArray.forEach((item, index) => {
    console.log(
      `${index + 1}. ${item.tagName}: ` +
        `gain=${item.informationGain.toFixed(3)}, ` +
        `appears in ${item.problemCount}/${totalProblems} problems`,
    );
  });

  return informationGainMap;
});

// Computed property to get top 20 tags by information gain
const top20TagsByInfoGain = computed(() => {
  const tagCount = sortedTagsByFrequency.value.filter(
    (item) => item.type === "tag",
  ).length;
  if (tagCount <= 20) {
    return new Set<string>(); // Don't highlight if 20 or fewer tags
  }

  const gainMap = tagInformationGain.value;

  const sortedTags = sortedTagsByFrequency.value
    .filter((item) => item.type === "tag" && item.tag)
    .map((item) => ({
      tagId: item.tag!.id,
      gain: gainMap.get(item.tag!.id) || 0,
    }))
    .sort((a, b) => b.gain - a.gain)
    .slice(0, 20)
    .map((item) => item.tagId);

  return new Set(sortedTags);
});

// Computed property to get top 15 tags for new search bar
const topTagsForNewSearch = computed(() => {
  console.log(sortedTagsByFrequency.value);
  const tags = sortedTagsByFrequency.value
    .filter((item) => item.type === "tag" && item.tag)
    .slice(0, 15)
    .map((item) => item.tag!); // Extract just the tag object

  console.log("[topTagsForNewSearch]", tags);

  // If no tags available, return default placeholder tags
  if (tags.length === 0) {
    return [
      { id: 'placeholder-1', name: 'no' },
      { id: 'placeholder-2', name: 'top' },
      { id: 'placeholder-3', name: 'tags' },
    ];
  }

  return tags;
});

// Computed property for search suggestions
const searchSuggestions = computed(() => {
  if (searchQuery.value.trim().length < 2) {
    return [];
  }

  const query = searchQuery.value.toLowerCase();
  const suggestions: Array<{
    id: string;
    name: string;
    type: "tag" | "job";
    infoGain?: number;
  }> = [];

  // Get information gain map
  const gainMap = tagInformationGain.value;

  // Add matching tags
  problemTags.value.forEach((tag) => {
    if (tag.name?.toLowerCase().includes(query)) {
      // Skip already selected tags
      if (!selectedTags.value.includes(tag.id)) {
        suggestions.push({
          id: tag.id,
          name: tag.name || "",
          type: "tag",
          infoGain: gainMap.get(tag.id) || 0,
        });
      }
    }
  });

  // Add matching job titles
  problems.value.forEach((problem) => {
    if (problem.name?.toLowerCase().includes(query)) {
      suggestions.push({
        id: problem.id,
        name: problem.name || "",
        type: "job",
        infoGain: 0, // Jobs don't have info gain
      });
    }
  });

  // Sort: tags first (by info gain), then jobs (alphabetically)
  return suggestions
    .sort((a, b) => {
      if (a.type !== b.type) {
        return a.type === "tag" ? -1 : 1;
      }
      if (a.type === "tag") {
        return (b.infoGain || 0) - (a.infoGain || 0); // Higher info gain first
      }
      return a.name.localeCompare(b.name);
    })
    .slice(0, 10); // Limit to 10 suggestions
});

// Computed property for debug info display
const debugInfoData = computed(() => {
  const totalProblems = filteredProblems.value.length;
  const currentEntropy = totalProblems > 1 ? Math.log2(totalProblems) : 0;

  const gainMap = tagInformationGain.value;

  const tagData = filteredTagsForDisplay.value
    .filter((item) => item.type === "tag" && item.tag)
    .map((item, index) => ({
      rank: index + 1,
      tagName: item.tag!.name || "",
      tagId: item.tag!.id,
      informationGain: gainMap.get(item.tag!.id) || 0,
      problemCount:
        sortedTagsByFrequency.value.find(
          (sortedItem) =>
            sortedItem.type === "tag" && sortedItem.tag?.id === item.tag!.id,
        )?.count || 0,
    }));

  return {
    totalProblems,
    currentEntropy,
    tags: tagData,
  };
});

// Computed property to filter displayed tags by search query (use appliedSearchQuery)
const filteredTagsForDisplay = computed(() => {
  let baseList = sortedTagsByFrequency.value;

  // Apply search filter if there is one
  if (appliedSearchQuery.value.trim() !== "") {
    const searchTerms = appliedSearchQuery.value
      .toLowerCase()
      .split(/\s+/)
      .filter((term) => term.trim() !== "");

    // Filter tags: keep tags whose names match the search query
    baseList = baseList.filter((item) => {
      if (item.type === "tag" && item.tag) {
        const tagName = item.tag.name?.toLowerCase() || "";
        // All search terms must match the tag name
        return searchTerms.every((term) => tagName.includes(term));
      } else if (item.type === "problem" && item.problem) {
        // For problem chips, always show them (they're already filtered via filteredProblems)
        return true;
      }
      return false;
    });

    console.log(
      "[filteredTagsForDisplay] Search query:",
      appliedSearchQuery.value,
      "result count:",
      baseList.length,
    );
  } else {
    console.log(
      "[filteredTagsForDisplay] No search query, returning all from sortedTagsByFrequency:",
      baseList.length,
    );
  }

  // Only show tags (do not display problem chips)
  const tags = baseList.filter((item) => item.type === "tag");

  if (tags.length === 0) {
    return [];
  }

  // Get information gain values for all tags
  const gainMap = tagInformationGain.value;
  const tagsWithGain = tags
    .map((item) => ({
      item,
      gain: gainMap.get(item.tag!.id) || 0,
    }))
    .filter((t) => t.gain > 0); // Only tags with positive info gain

  if (tagsWithGain.length === 0) {
    return [];
  }

  // Calculate meaningful threshold
  // We want to filter out tags that provide minimal information gain
  const gains = tagsWithGain.map((t) => t.gain).sort((a, b) => b - a);
  const maxGain = gains[0];

  // Set threshold at 10% of max gain, or use a minimum absolute threshold
  const relativeThreshold = maxGain * 0.1;
  const absoluteThreshold = 0.05; // Minimum absolute threshold
  const threshold = Math.max(relativeThreshold, absoluteThreshold);

  // Filter tags above threshold
  const impactfulTags = tagsWithGain.filter((t) => t.gain >= threshold);

  console.log(
    "[filteredTagsForDisplay] Max gain:",
    maxGain.toFixed(3),
    "Threshold:",
    threshold.toFixed(3),
    "Impactful tags:",
    impactfulTags.length,
    "of",
    tags.length,
    "total tags",
  );

  // If we have impactful tags, show them (up to 25)
  if (impactfulTags.length > 0) {
    const result = impactfulTags.slice(0, 25).map((t) => t.item);
    console.log(
      "[filteredTagsForDisplay] Showing",
      result.length,
      "impactful tags",
    );
    return result;
  }

  // If no tags meet threshold but we have tags, show 1-3 random ones
  const randomCount = Math.min(3, tags.length);
  const shuffled = [...tags].sort(() => Math.random() - 0.5);
  const result = shuffled.slice(0, randomCount);
  console.log(
    "[filteredTagsForDisplay] No tags above threshold, showing",
    result.length,
    "random tags",
  );
  return result;
});

// Helper function to check if a tag is selected
const isTagSelected = (tagId: string): boolean => {
  return selectedTags.value.includes(tagId);
};

// Helper function to get chip color based on type and selection state
const getChipColor = (itemData: {
  tag?: ProblemTagsRecord;
  problem?: ProblemsRecord;
  type: "tag" | "problem";
}) => {
  if (itemData.type === "problem") {
    return "success"; // Green/success color for job titles
  } else if (itemData.type === "tag" && itemData.tag) {
    return isTagSelected(itemData.tag.id) ? "tertiary" : "primary";
  }
  return "primary";
};

// Helper function to check if a tag is in top 20 by information gain
const isTop20Tag = (itemData: {
  tag?: ProblemTagsRecord;
  problem?: ProblemsRecord;
  type: "tag" | "problem";
}) => {
  if (itemData.type === "tag" && itemData.tag) {
    return top20TagsByInfoGain.value.has(itemData.tag.id);
  }
  return false;
};

// Tag/job selection function (use store for tag selection)
const selectTagProblems = async (itemData: {
  tag?: ProblemTagsRecord;
  problem?: ProblemsRecord;
  problems: ProblemsRecord[];
  count: number;
  type: "tag" | "problem";
}) => {
  if (itemData.type === "problem" && itemData.problem) {
    // If it's a job title chip, select it directly
    selectJob(itemData.problem);
  } else if (itemData.type === "tag" && itemData.tag) {
    // If it's a tag, toggle tag selection using store
    const tagId = itemData.tag.id;
    await sessionStore.selectTag(tagId);

    // Log filtered problems after tag selection
    console.log("=== TAG CLICKED ===");
    console.log("Tag:", itemData.tag.name);
    console.log(
      "Selected tags:",
      selectedTags.value.map((id) => getTagNameById(id)),
    );
    console.log("Filtered problems count:", filteredProblems.value.length);
    console.log(
      "Filtered problems:",
      filteredProblems.value.map((p) => ({
        name: p.name,
        description: p.description,
        tags: getTagsForProblem(p),
        tagIds: getProblemTagIds(p),
      })),
    );
    console.log(
      "Shown tags count:",
      sortedTagsByFrequency.value.filter((item) => item.type === "tag").length,
    );
    console.log(
      "Shown tags:",
      sortedTagsByFrequency.value
        .filter((item) => item.type === "tag")
        .map((item) => ({
          name: item.tag?.name,
          id: item.tag?.id,
          count: item.count,
        })),
    );
  }
};

// Job selection function
const formatDescriptionWithHashtags = (description: string | undefined) => {
  if (!description) return "";
  // Find the first # and style everything from there onwards
  const hashIndex = description.indexOf("#");
  if (hashIndex === -1) return description;

  const beforeHash = description.substring(0, hashIndex);
  const afterHash = description.substring(hashIndex);

  return beforeHash + '<span class="hashtag">' + afterHash + "</span>";
};

const selectJob = (problem: ProblemsRecord) => {
  console.log("Selected job:", problem.name);
  // Navigate to view job page without animation
  router.push({
    path: `/view-job/${problem.id}`,
    // @ts-ignore - Ionic router options
    routerDirection: "none",
  });
};

// Info modal functions
const openInfoModal = () => {
  isInfoModalOpen.value = true;
  showDebugInfo.value = false; // Reset to info view when opening
  generateCsvPreview(); // Generate CSV preview when modal opens
};

const closeInfoModal = () => {
  isInfoModalOpen.value = false;
  showDebugInfo.value = false;
};

const toggleDebugInfo = () => {
  showDebugInfo.value = !showDebugInfo.value;
};

// Generate CSV preview
const generateCsvPreview = async () => {
  isLoadingCsv.value = true;
  csvPreviewData.value = "Loading menu names...";

  try {
    await sessionStore.initDb();

    // CSV header
    let csv = "Job Name,Menu Name,Band-Aid Offer Name\n";

    // Process each filtered problem (respects search terms and tags)
    for (const problem of filteredProblems.value) {
      const jobName = problem.name || "Untitled Job";
      let menuName = "No Menu";
      let bandAidOfferName = "No Offer";

      // Use the reusable session store function to fetch menu data
      const menuData = await sessionStore.fetchMenuDataForProblem(problem);
      if (menuData && menuData.name) {
        menuName = menuData.name;

        // Find the band-aid tier and get its offer name
        if (menuData.tiers && Array.isArray(menuData.tiers)) {
          const bandAidTier = menuData.tiers.find(
            (tier: any) =>
              tier.name && tier.name.toLowerCase().includes("band"),
          );

          if (bandAidTier && bandAidTier.offer && bandAidTier.offer.name) {
            bandAidOfferName = bandAidTier.offer.name;
          }
        }
      }

      // Escape quotes and add row to CSV
      const escapedJobName = jobName.replace(/"/g, '""');
      const escapedMenuName = menuName.replace(/"/g, '""');
      const escapedOfferName = bandAidOfferName.replace(/"/g, '""');
      csv += `"${escapedJobName}","${escapedMenuName}","${escapedOfferName}"\n`;
    }

    csvPreviewData.value = csv;
  } catch (error) {
    console.error("Error generating CSV preview:", error);
    csvPreviewData.value = "Error generating CSV preview";
  } finally {
    isLoadingCsv.value = false;
  }
};

// Copy CSV to clipboard
const copyCsvToClipboard = async () => {
  try {
    await navigator.clipboard.writeText(csvPreviewData.value);
    alert("CSV data copied to clipboard!");
  } catch (error) {
    console.error("Error copying to clipboard:", error);
    alert("Failed to copy to clipboard");
  }
};

// Export jobs function with menu data
const handleExportJobs = async () => {
  try {
    await sessionStore.initDb();

    // Fetch menu data for each filtered problem (respects search terms and tags)
    const enhancedProblems = [];
    for (const problem of filteredProblems.value) {
      const menuData = await sessionStore.fetchMenuDataForProblem(problem);
      enhancedProblems.push({
        ...problem,
        menuData: menuData || null,
      });
    }

    // Create export object with metadata
    const exportData = {
      exportDate: new Date().toISOString(),
      totalProblems: enhancedProblems.length,
      filters: {
        selectedTags: selectedTags.value,
        searchQuery: appliedSearchQuery.value,
      },
      problems: enhancedProblems,
    };

    // Log to console
    console.log("=== Jobs Export Data ===");
    console.log(exportData);
    console.log("========================");

    // Create a JSON string with pretty formatting
    const jsonString = JSON.stringify(exportData, null, 2);

    // Create a blob from the JSON string
    const blob = new Blob([jsonString], { type: "application/json" });

    // Create a download link
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `jobs-export-${new Date().toISOString().split("T")[0]}.json`;

    // Trigger the download
    document.body.appendChild(link);
    link.click();

    // Clean up
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  } catch (error) {
    console.error("Error exporting jobs:", error);
  }
};

// Bookmarks modal functions
const openBookmarksModal = () => {
  isBookmarksModalOpen.value = true;
};

const closeBookmarksModal = () => {
  isBookmarksModalOpen.value = false;
};

// Get 5 random jobs as bookmarks (mock data)
const bookmarkedJobs = computed(() => {
  if (problems.value.length === 0) return [];

  // Create a copy and shuffle
  const shuffled = [...problems.value].sort(() => 0.5 - Math.random());

  // Return first 5
  return shuffled.slice(0, 5);
});

// Select a bookmarked job
const selectBookmarkedJob = (problem: ProblemsRecord) => {
  closeBookmarksModal();
  selectJob(problem);
};

// Apply the search query (use store)
const applySearch = async () => {
  if (searchQuery.value.trim() === "") return;

  console.log(
    "[applySearch] searchQuery:",
    searchQuery.value,
    "appliedSearchQuery before:",
    appliedSearchQuery.value,
  );

  // Use store action to set search query
  await sessionStore.setSearchQuery(searchQuery.value);

  // Clear the input for next search term
  searchQuery.value = "";

  console.log(
    "[applySearch] appliedSearchQuery after:",
    appliedSearchQuery.value,
  );
};

// Handle Enter key on search input
const handleSearchKeydown = (event: KeyboardEvent) => {
  if (!showSuggestions.value || searchSuggestions.value.length === 0) {
    if (event.key === "Enter") {
      applySearch();
    }
    return;
  }

  // Handle keyboard navigation in suggestions
  if (event.key === "ArrowDown") {
    event.preventDefault();
    selectedSuggestionIndex.value = Math.min(
      selectedSuggestionIndex.value + 1,
      searchSuggestions.value.length - 1,
    );
  } else if (event.key === "ArrowUp") {
    event.preventDefault();
    selectedSuggestionIndex.value = Math.max(
      selectedSuggestionIndex.value - 1,
      -1,
    );
  } else if (event.key === "Enter") {
    event.preventDefault();
    if (selectedSuggestionIndex.value >= 0) {
      selectSuggestion(searchSuggestions.value[selectedSuggestionIndex.value]);
    } else {
      applySearch();
    }
  } else if (event.key === "Escape") {
    showSuggestions.value = false;
    selectedSuggestionIndex.value = -1;
  }
};

// Handle search input changes
const handleSearchInput = () => {
  showSuggestions.value = searchQuery.value.trim().length >= 2;
  selectedSuggestionIndex.value = -1;
};

// Handle search input focus
const handleSearchFocus = () => {
  if (searchQuery.value.trim().length >= 2) {
    showSuggestions.value = true;
  }
};

// Handle search input blur (with delay for click handling)
const handleSearchBlur = () => {
  setTimeout(() => {
    showSuggestions.value = false;
    selectedSuggestionIndex.value = -1;
  }, 200);
};

// Select a suggestion (use store for tags)
const selectSuggestion = async (suggestion: {
  id: string;
  name: string;
  type: "tag" | "job";
}) => {
  if (suggestion.type === "tag") {
    // Add tag to selected tags using store
    if (!selectedTags.value.includes(suggestion.id)) {
      await sessionStore.selectTag(suggestion.id);
    }
    searchQuery.value = "";
    showSuggestions.value = false;
    selectedSuggestionIndex.value = -1;
  } else if (suggestion.type === "job") {
    // Find the problem and select it
    const problem = problems.value.find((p) => p.id === suggestion.id);
    if (problem) {
      selectJob(problem);
    }
    searchQuery.value = "";
    showSuggestions.value = false;
    selectedSuggestionIndex.value = -1;
  }
};

const clearSearch = async () => {
  console.log(
    "[clearSearch] Before clear - appliedSearchQuery:",
    appliedSearchQuery.value,
    "selectedTags:",
    selectedTags.value.length,
  );

  // Reset all state to initial values using store
  searchQuery.value = "";
  await sessionStore.clearSearch();

  console.log(
    "[clearSearch] After clear - appliedSearchQuery:",
    appliedSearchQuery.value,
    "selectedTags:",
    selectedTags.value.length,
  );
};

// Remove a selected tag (deselect it)
const removeSelectedTag = async (tagId: string) => {
  await sessionStore.selectTag(tagId); // Toggle it off
};

// Select a tag (toggle it on/off)
const selectTag = async (tagId: string) => {
  await sessionStore.selectTag(tagId);
};

// Get tag name by ID
const getTagNameById = (tagId: string): string => {
  const tag = problemTags.value.find((t) => t.id === tagId);
  if (!tag || !tag.name) {
    console.warn("[getTagNameById] Tag not found or has no name:", tagId);
    return tagId; // Fallback to tagId if name not found
  }
  return tag.name;
};

// Sync selectedTags to URL (using tag names)
watch(selectedTags, (newTags) => {
  if (isSyncingFromUrl.value) return;

  // Convert tag IDs to tag names
  const newTagNames = newTags
    .map((tagId) => problemTags.value.find((t) => t.id === tagId)?.name)
    .filter(Boolean);

  const currentTagNames =
    route.query.tags?.toString().split(",").filter(Boolean) || [];
  const newTagNamesStr = newTagNames.join(",");

  // Only update if different
  if (newTagNamesStr !== currentTagNames.join(",")) {
    router.replace({
      query: {
        ...route.query,
        tags: newTagNamesStr || undefined, // undefined removes param
      },
    });
  }
});

// Sync appliedSearchQuery to URL
watch(appliedSearchQuery, (newQuery) => {
  if (isSyncingFromUrl.value) return;

  const currentQuery = route.query.q?.toString() || "";

  if (newQuery !== currentQuery) {
    router.replace({
      query: {
        ...route.query,
        q: newQuery || undefined,
      },
    });
  }
});

// Sync from URL to state (handles both initial load and cached view navigation)
watch(
  () => route.query,
  async (newQuery) => {
    // Wait for data to be loaded before syncing
    if (problems.value.length === 0 || problemTags.value.length === 0) {
      return;
    }

    isSyncingFromUrl.value = true;

    try {
      // Parse tag names from URL
      const urlTagNames =
        newQuery.tags?.toString().split(",").filter(Boolean) || [];
      const urlQuery = newQuery.q?.toString() || "";

      // Convert tag names to tag IDs
      const urlTagIds = urlTagNames
        .map(
          (tagName) =>
            problemTags.value.find((tag) => tag.name === tagName)?.id,
        )
        .filter(Boolean) as string[];

      // Update tags if different
      if (
        JSON.stringify(urlTagIds.sort()) !==
        JSON.stringify([...selectedTags.value].sort())
      ) {
        // Clear existing tags and apply URL tags
        const currentTags = [...selectedTags.value];

        // Remove tags that aren't in URL
        for (const tagId of currentTags) {
          if (!urlTagIds.includes(tagId)) {
            await sessionStore.selectTag(tagId);
          }
        }

        // Add tags from URL that aren't selected
        for (const tagId of urlTagIds) {
          if (!selectedTags.value.includes(tagId)) {
            await sessionStore.selectTag(tagId);
          }
        }
      }

      // Update search query if different
      if (urlQuery !== appliedSearchQuery.value) {
        if (urlQuery) {
          // Clear existing query first
          if (appliedSearchQuery.value) {
            await sessionStore.clearSearch();
          }
          await sessionStore.setSearchQuery(urlQuery);
        } else if (appliedSearchQuery.value) {
          // URL has no query but we have one, clear it
          await sessionStore.clearSearch();
        }
      }
    } finally {
      isSyncingFromUrl.value = false;
    }
  },
  { immediate: true },
);

onMounted(async () => {
  // Load problems and tags from store (which loads from database)
  await sessionStore.loadProblemsAndTags();
});
</script>

<style scoped>
.find-job-container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  height: calc(100vh - 40px);
}

.title-section {
  margin-bottom: 32px;
}

.title-container {
  position: relative;
  margin-bottom: 24px;
}

.title-icons {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: 8px;
}

.title-icons ion-icon {
  font-size: 48px;
  color: var(--ion-color-primary);
}

.back-icon-wrapper {
  position: relative;
  display: inline-block;
  background-color: var(--ion-color-tertiary);
  padding: 8px;
  border-radius: 4px;
}

.star-icon-wrapper {
  position: relative;
  display: inline-block;
}

.star-icon-shadow {
  position: absolute;
  top: 2px;
  left: -2px;
  font-size: 48px;
  z-index: 0;
}

.star-icon-front {
  position: relative;
  z-index: 1;
}

.title-icons-right {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: 8px;
}

.title-icons-right ion-icon {
  font-size: 48px;
  color: var(--ion-color-primary);
}

.clickable-icon {
  cursor: pointer;
  transition:
    transform 0.2s ease,
    color 0.2s ease;
}

.clickable-icon:hover {
  transform: scale(1.1);
  color: var(--ion-color-primary-shade);
}

.clickable-icon:active {
  transform: scale(0.95);
}

.page-title {
  margin: 0;
  color: #ffffff;
  font-size: 48px;
  font-weight: 700;
  text-align: center;
  font-variant: small-caps;
}

/* Suggestions dropdown */
.suggestions-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 1000;
  margin-top: 4px;
  background: var(--ion-color-dark);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  border: 1px solid var(--ion-color-medium);
  max-height: 400px;
  overflow-y: auto;
}

.suggestions-dropdown ion-list {
  background: transparent;
  padding: 0;
}

.suggestion-item {
  --background: var(--ion-color-dark);
  --border-color: transparent;
  cursor: pointer;
  transition: background 0.2s ease;
}

.suggestion-item:hover,
.suggestion-item.suggestion-selected {
  --background: var(--ion-color-medium);
}

.suggestion-item ion-label {
  font-size: 15px;
  color: var(--ion-color-light);
}

.suggestion-item .tag-icon {
  color: var(--ion-color-primary);
  font-size: 20px;
}

.suggestion-item .job-icon {
  color: var(--ion-color-success);
  font-size: 20px;
}

/* New Search Bar Section */
.new-search-bar-section {
  margin-top: 32px;
  margin-bottom: 32px;
  width: 100%;
}

.new-search-title {
  margin: 0 0 24px 0;
  color: #ffffff;
  font-size: 32px;
  font-weight: 600;
  text-align: center;
  font-variant: small-caps;
}

.new-search-tags-container {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  justify-content: center;
  align-items: center;
  padding: 24px;
  background-color: #000000;
  border-radius: 12px;
}

.new-search-tag-chip {
  cursor: pointer;
  margin: 0;
  text-transform: lowercase;
  font-size: 20px;
  height: 48px;
  padding: 0 24px;
  white-space: nowrap;
}

.new-search-tag-chip:hover {
  filter: brightness(1.1);
  transform: scale(1.02);
  transition: all 0.2s ease;
}

.new-search-term-chip {
  /* Blue/secondary color chips for search terms */
}

.new-search-bar {
  display: flex;
  gap: 8px;
  align-items: center;
  background-color: var(--ion-color-dark);
  padding: 12px;
  border-radius: 8px;
  margin-top: 24px;
}

.new-search-input-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  position: relative;
}

.new-search-input {
  width: 100%;
  font-size: 20px;
  min-height: 48px;
}

.new-search-undo-wrapper {
  position: relative;
  display: inline-block;
}

.new-search-undo-button {
  margin: 0;
  min-width: 44px;
}

.new-search-undo-shadow {
  position: absolute;
  top: 2px;
  left: -2px;
  z-index: 0;
}

.new-search-undo-front {
  position: relative;
  z-index: 1;
  --color: var(--ion-color-tertiary);
}

/* Available jobs section - Google search result style */
.available-jobs-section {
  margin-top: 24px;
  width: 100%;
}

.search-results {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.search-result-item {
  cursor: pointer;
  padding: 12px 0;
  border-bottom: 1px solid var(--ion-color-medium);
  transition: background-color 0.1s ease;
  display: flex;
  align-items: center;
  gap: 16px;
}

.search-result-item:hover {
  background-color: rgba(118, 221, 84, 0.1);
  padding-left: 8px;
  padding-right: 8px;
  margin-left: -8px;
  margin-right: -8px;
  border-radius: 4px;
}

.search-result-item:last-child {
  border-bottom: none;
}

.result-content {
  flex: 1;
  min-width: 0;
}

.result-title {
  margin: 0 0 4px 0;
  font-size: 20px;
  font-weight: 400;
  color: #ffffff;
  line-height: 1.3;
}

.search-result-item:hover .result-title {
  text-decoration: underline;
}

.result-description {
  margin: 0;
  font-size: 14px;
  line-height: 1.58;
  color: var(--ion-color-light);
  max-width: 600px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.details-button {
  --padding-start: 20px;
  --padding-end: 20px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  flex-shrink: 0;
}

.result-description :deep(.hashtag) {
  color: var(--ion-color-primary);
  font-size: 12px;
}

.no-jobs-message {
  text-align: center;
  padding: 20px;
  color: var(--ion-color-medium);
}

.no-jobs-message p {
  margin: 0;
  font-size: 14px;
}

/* Tablet adjustments */
@media (min-width: 768px) and (max-width: 1024px) {
  .find-job-container {
    padding: 20px 12px;
  }
}

/* Mobile adjustments */
@media (max-width: 767px) {
  .find-job-container {
    padding: 16px 16px 140px 16px;
  }
}

/* Job Details Modal Styles */
.job-details h2 {
  margin: 0 0 16px 0;
  font-size: 28px;
  font-weight: 700;
  color: var(--ion-color-primary);
}

.job-description {
  margin-top: 16px;
}

.job-description p {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ion-color-light);
  margin: 0;
}

.select-job-button {
  margin: 8px;
  font-weight: 600;
  font-size: 18px;
}

/* Info Modal Styles */
.info-content {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ion-color-light);
}

.info-content p {
  margin-bottom: 16px;
}

.info-content p:last-child {
  margin-bottom: 0;
}

/* Debug Info Styles */
.debug-content {
  font-size: 14px;
  color: var(--ion-color-light);
}

.debug-summary {
  margin-bottom: 24px;
  padding: 16px;
  background-color: var(--ion-color-dark);
  border-radius: 8px;
}

.debug-summary p {
  margin: 8px 0;
  color: var(--ion-color-light);
}

.debug-summary p:first-child {
  margin-top: 0;
}

.debug-summary p:last-child {
  margin-bottom: 0;
}

.debug-summary strong {
  color: var(--ion-color-primary);
}

.debug-tags-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.debug-tag-item {
  display: flex;
  gap: 12px;
  padding: 12px;
  background-color: var(--ion-color-dark);
  border-radius: 8px;
  border-left: 3px solid var(--ion-color-primary);
}

.debug-tag-rank {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--ion-color-primary);
  color: #000000;
  border-radius: 50%;
  font-weight: 600;
  font-size: 14px;
}

.debug-tag-details {
  flex: 1;
}

.debug-tag-name {
  font-weight: 600;
  font-size: 16px;
  margin-bottom: 4px;
  color: var(--ion-color-light);
}

.debug-tag-stats {
  font-size: 13px;
  color: var(--ion-color-medium);
}

.debug-divider {
  margin: 0 8px;
  color: var(--ion-color-medium);
}

/* Action Bar */
.action-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: var(--ion-color-primary);
  padding: 20px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
  z-index: 20001;
  cursor: pointer;
  transition: transform 0.1s ease;
}

.action-bar:hover {
  background-color: var(--ion-color-primary-shade);
}

.action-bar:active {
  transform: scale(0.98);
}

.action-bar-disabled {
  background-color: var(--ion-color-medium);
  cursor: not-allowed;
}

.action-bar-disabled:hover {
  background-color: var(--ion-color-medium);
}

.action-bar-disabled:active {
  transform: none;
}

.action-bar-content {
  max-width: 1000px;
  margin: 0 auto;
  text-align: center;
}

.action-bar-text {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  color: #ffffff;
  font-variant: small-caps;
}

/* Bookmarks Modal Styles */
.bookmarks-content {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ion-color-light);
}

.bookmarks-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.bookmark-item {
  display: flex;
  gap: 16px;
  padding: 16px;
  background-color: var(--ion-color-dark);
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.2s ease;
  border-left: 3px solid var(--ion-color-warning);
}

.bookmark-item:hover {
  background-color: rgba(118, 221, 84, 0.1);
}

.bookmark-star {
  font-size: 24px;
  flex-shrink: 0;
  margin-top: 4px;
}

.bookmark-details {
  flex: 1;
}

.bookmark-title {
  margin: 0 0 8px 0;
  font-size: 20px;
  font-weight: 600;
  color: var(--ion-color-light);
}

.bookmark-description {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ion-color-medium);
  max-width: 600px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.bookmark-description :deep(.hashtag) {
  color: var(--ion-color-primary);
  font-size: 12px;
}

/* CSV Preview Styles */
.csv-preview-container {
  margin: 16px 0;
  border: 1px solid var(--ion-color-medium);
  border-radius: 8px;
  background-color: var(--ion-color-light);
  overflow: hidden;
}

.csv-preview {
  margin: 0;
  padding: 12px;
  font-family: "Courier New", monospace;
  font-size: 12px;
  line-height: 1.4;
  color: var(--ion-color-dark);
  max-height: 300px;
  overflow-y: auto;
  overflow-x: auto;
  white-space: pre;
}

.export-buttons {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 16px;
}

@media (min-width: 768px) {
  .export-buttons {
    flex-direction: row;
  }
}
</style>